diff --git a/.eslintignore b/.eslintignore index e5b17567b562cf..86a01b68ecab1f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,7 @@ bower_components /built_assets /html_docs /src/plugins/data/common/es_query/kuery/ast/_generated_/** +/src/legacy/core_plugins/vis_type_timelion/public/_generated_/** src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts @@ -16,7 +17,7 @@ src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/moc /src/legacy/core_plugins/console/public/webpackShims /src/legacy/core_plugins/console/public/tests/webpackShims /src/legacy/ui/public/utils/decode_geo_hash.js -/src/legacy/core_plugins/timelion/public/webpackShims/jquery.flot.* +/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.* /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ed5721e8756e88..7276a726fd6d1f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /src/legacy/core_plugins/kibana/public/home/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/metrics/ @elastic/kibana-app +/src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/home/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 59123731dce666..708c9efea404ba 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -15,7 +15,6 @@ jobs: [ { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, - { "label": "Team:Platform", "projectName": "kibana-platform", "columnId": 5514360 }, - {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580} + { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } ] ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.i18nrc.json b/.i18nrc.json index 6986d36e8e94f5..907310b32e35cc 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -21,7 +21,6 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "kbnVislibVisTypes": "src/legacy/core_plugins/vis_type_vislib", "management": ["src/legacy/core_plugins/management", "src/plugins/management"], "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", @@ -33,7 +32,7 @@ "statusPage": "src/legacy/core_plugins/status_page", "telemetry": "src/legacy/core_plugins/telemetry", "tileMap": "src/legacy/core_plugins/tile_map", - "timelion": "src/legacy/core_plugins/timelion", + "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion"], "uiActions": "src/plugins/ui_actions", "visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown", "visTypeMetric": "src/legacy/core_plugins/vis_type_metric", @@ -41,6 +40,7 @@ "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", "visTypeTimeseries": "src/legacy/core_plugins/vis_type_timeseries", "visTypeVega": "src/legacy/core_plugins/vis_type_vega", + "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visualizations": [ "src/plugins/visualizations", "src/legacy/core_plugins/visualizations" @@ -50,4 +50,4 @@ "src/legacy/ui/ui_render/ui_render_mixin.js" ], "translations": [] -} \ No newline at end of file +} diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index 412019efc7f353..c46ba8a980ce22 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -23,9 +23,9 @@ Want to export multiple workpads? Go to the *Canvas workpads* view, select the w [[create-workpad-pdf]] === Create a PDF -Create a PDF copy of your workpad that you can save and share outside of {kib}. +If you have a license that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}. -. If you are using a Gold or Platinum license, enable reporting in your `config/kibana.yml` file. +For more information, refer to <>. . From your workpad, click the *Share workpad* icon in the upper left corner, then select *PDF reports*. @@ -38,12 +38,10 @@ image::images/canvas-generate-pdf.gif[Generate PDF] [[create-workpad-URL]] === Create a POST URL -Create a POST URL that you can use to automatically generate PDF reports using Watcher or a script. +If you have a license that supports the {report-features}, you can create a POST URL that you can use to automatically generate PDF reports using Watcher or a script. For more information, refer to <>. -. If you are using a Gold or Platinum license, enable reporting in your `config/kibana.yml` file. - . From your workpad, click the *Share workpad* icon in the upper left corner, then select *PDF reports*. . Click *Copy POST URL*. @@ -57,8 +55,6 @@ image::images/canvas-create-URL.gif[Create POST URL] beta[] Canvas allows you to create _shareables_, which are workpads that you download and securely share on any website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar. -. If you are using a Gold or Platinum license, enable reporting in your `config/kibana.yml` file. - . From your workpad, click the *Share this workpad* icon in the upper left corner, then select *Share on a website*. . On the *Share on a website* pane, follow the instructions. diff --git a/docs/developer/visualize/development-create-visualization.asciidoc b/docs/developer/visualize/development-create-visualization.asciidoc index b782428b831352..faaa9b36a7a00a 100644 --- a/docs/developer/visualize/development-create-visualization.asciidoc +++ b/docs/developer/visualize/development-create-visualization.asciidoc @@ -208,8 +208,8 @@ This is the sidebar editor you see in many of the Kibana visualizations. You can [[development-default-editor]] ==== `default` editor controller -The default editor controller receives an `optionsTemplate` or `optionsTabs` parameter. -These can be either an AngularJS template or React component. +The default editor controller receives an `optionsTemplate` or `optionTabs` parameter. +These tabs should be React components. ["source","js"] ----------- @@ -220,12 +220,9 @@ These can be either an AngularJS template or React component. description: 'Cool new chart', editor: 'default', editorConfig: { - optionsTemplate: '' // or optionsTemplate: MyReactComponent // or if multiple tabs are required: - optionsTabs: [ - { title: 'tab 1', template: '
....
}, - { title: 'tab 2', template: '' }, - { title: 'tab 3', template: MyReactComponent } + optionTabs: [ + { title: 'tab 3', editor: MyReactComponent } ] } } diff --git a/docs/development/core/public/kibana-plugin-public.appbase.category.md b/docs/development/core/public/kibana-plugin-public.appbase.category.md new file mode 100644 index 00000000000000..215ebbbd0e1863 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.category.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [category](./kibana-plugin-public.appbase.category.md) + +## AppBase.category property + +The category definition of the product See [AppCategory](./kibana-plugin-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference + +Signature: + +```typescript +category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.md b/docs/development/core/public/kibana-plugin-public.appbase.md index eb6d91cb924888..6f547450b6a129 100644 --- a/docs/development/core/public/kibana-plugin-public.appbase.md +++ b/docs/development/core/public/kibana-plugin-public.appbase.md @@ -16,6 +16,7 @@ export interface AppBase | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | +| [category](./kibana-plugin-public.appbase.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | | [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [icon](./kibana-plugin-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md b/docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md new file mode 100644 index 00000000000000..0245b548ae74f5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.arialabel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [ariaLabel](./kibana-plugin-public.appcategory.arialabel.md) + +## AppCategory.ariaLabel property + +If the visual label isn't appropriate for screen readers, can override it here + +Signature: + +```typescript +ariaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md b/docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md new file mode 100644 index 00000000000000..90133735a00824 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.euiicontype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [euiIconType](./kibana-plugin-public.appcategory.euiicontype.md) + +## AppCategory.euiIconType property + +Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined + +Signature: + +```typescript +euiIconType?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.label.md b/docs/development/core/public/kibana-plugin-public.appcategory.label.md new file mode 100644 index 00000000000000..171b1627f9ef8a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.label.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [label](./kibana-plugin-public.appcategory.label.md) + +## AppCategory.label property + +Label used for cateogry name. Also used as aria-label if one isn't set. + +Signature: + +```typescript +label: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.md b/docs/development/core/public/kibana-plugin-public.appcategory.md new file mode 100644 index 00000000000000..f1085e73252721 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) + +## AppCategory interface + +A category definition for nav links to know where to sort them in the left hand nav + +Signature: + +```typescript +export interface AppCategory +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [ariaLabel](./kibana-plugin-public.appcategory.arialabel.md) | string | If the visual label isn't appropriate for screen readers, can override it here | +| [euiIconType](./kibana-plugin-public.appcategory.euiicontype.md) | string | Define an icon to be used for the category If the category is only 1 item, and no icon is defined, will default to the product icon Defaults to initials if no icon is defined | +| [label](./kibana-plugin-public.appcategory.label.md) | string | Label used for cateogry name. Also used as aria-label if one isn't set. | +| [order](./kibana-plugin-public.appcategory.order.md) | number | The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) | + diff --git a/docs/development/core/public/kibana-plugin-public.appcategory.order.md b/docs/development/core/public/kibana-plugin-public.appcategory.order.md new file mode 100644 index 00000000000000..ef17ac04b78d6a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appcategory.order.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppCategory](./kibana-plugin-public.appcategory.md) > [order](./kibana-plugin-public.appcategory.order.md) + +## AppCategory.order property + +The order that categories will be sorted in Prefer large steps between categories to allow for further editing (Default categories are in steps of 1000) + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.category.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.category.md new file mode 100644 index 00000000000000..19d5a43a293079 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.category.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [category](./kibana-plugin-public.chromenavlink.category.md) + +## ChromeNavLink.category property + +The category the app lives in + +Signature: + +```typescript +readonly category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index 4cb9080222ac54..2afd6ce2d58c46 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -17,6 +17,7 @@ export interface ChromeNavLink | --- | --- | --- | | [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen. | | [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | +| [category](./kibana-plugin-public.chromenavlink.category.md) | AppCategory | The category the app lives in | | [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | | [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md b/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md index 2deb9f4a9a1512..155d149f334a17 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlinks.update.md @@ -4,6 +4,11 @@ ## ChromeNavLinks.update() method +> Warning: This API is now obsolete. +> +> Uses the [AppBase.updater$](./kibana-plugin-public.appbase.updater_.md) property when registering your application with [ApplicationSetup.register()](./kibana-plugin-public.applicationsetup.register.md) instead. +> + Update the navlink for the given id with the updated attributes. Returns the updated navlink or `undefined` if it does not exist. Signature: diff --git a/docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md b/docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md new file mode 100644 index 00000000000000..540b17b5a6f0b3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.fatalerrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [fatalErrors](./kibana-plugin-public.corestart.fatalerrors.md) + +## CoreStart.fatalErrors property + +[FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) + +Signature: + +```typescript +fatalErrors: FatalErrorsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index e561ee313f1000..83af82d590c364 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -19,6 +19,7 @@ export interface CoreStart | [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [docLinks](./kibana-plugin-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | +| [fatalErrors](./kibana-plugin-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) | | [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [injectedMetadata](./kibana-plugin-public.corestart.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. | diff --git a/docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md b/docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md new file mode 100644 index 00000000000000..a8ece7dcb7e02d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.fatalerrorsstart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) + +## FatalErrorsStart type + +FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. + +Signature: + +```typescript +export declare type FatalErrorsStart = FatalErrorsSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.category.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.category.md new file mode 100644 index 00000000000000..7026e9b519cc03 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.category.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) > [category](./kibana-plugin-public.legacynavlink.category.md) + +## LegacyNavLink.category property + +Signature: + +```typescript +category?: AppCategory; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacynavlink.md b/docs/development/core/public/kibana-plugin-public.legacynavlink.md index fc0c445f517b3b..e112110dd10f85 100644 --- a/docs/development/core/public/kibana-plugin-public.legacynavlink.md +++ b/docs/development/core/public/kibana-plugin-public.legacynavlink.md @@ -15,6 +15,7 @@ export interface LegacyNavLink | Property | Type | Description | | --- | --- | --- | +| [category](./kibana-plugin-public.legacynavlink.category.md) | AppCategory | | | [euiIconType](./kibana-plugin-public.legacynavlink.euiicontype.md) | string | | | [icon](./kibana-plugin-public.legacynavlink.icon.md) | string | | | [id](./kibana-plugin-public.legacynavlink.id.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 27ca9f2d9fd577..52aca7501e64dd 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -32,6 +32,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. | | [AppBase](./kibana-plugin-public.appbase.md) | | +| [AppCategory](./kibana-plugin-public.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AppLeaveConfirmAction](./kibana-plugin-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to show a confirmation message when trying to leave an application.See | | [AppLeaveDefaultAction](./kibana-plugin-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-public.appleavehandler.md) to execute the default behaviour when leaving the application.See | | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | @@ -129,6 +130,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-public.chromehelpextensionmenugithublink.md) | | | [ChromeHelpExtensionMenuLink](./kibana-plugin-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | +| [FatalErrorsStart](./kibana-plugin-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md b/docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md new file mode 100644 index 00000000000000..b05d28899f9d23 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [getStartServices](./kibana-plugin-server.coresetup.getstartservices.md) + +## CoreSetup.getStartServices() method + +Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed `start`. This should only be used inside handlers registered during `setup` that will only be executed after `start` lifecycle. + +Signature: + +```typescript +getStartServices(): Promise<[CoreStart, TPluginsStart]>; +``` +Returns: + +`Promise<[CoreStart, TPluginsStart]>` + diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 3f7f5b727ee804..c36d649837e8a8 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -1,26 +1,32 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) - -## CoreSetup interface - -Context passed to the plugins `setup` method. - -Signature: - -```typescript -export interface CoreSetup -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | -| [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | -| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | -| [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | -| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | -| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | -| [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | - + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) + +## CoreSetup interface + +Context passed to the plugins `setup` method. + +Signature: + +```typescript +export interface CoreSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | +| [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | +| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | +| [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | +| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | +| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | +| [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | + +## Methods + +| Method | Description | +| --- | --- | +| [getStartServices()](./kibana-plugin-server.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed start. This should only be used inside handlers registered during setup that will only be executed after start lifecycle. | + diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md index b0830b8d72238b..4de20b9c6cccfe 100644 --- a/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md @@ -4,7 +4,7 @@ ## DiscoveredPlugin.configPath property -Root configuration path used by the plugin, defaults to "id". +Root configuration path used by the plugin, defaults to "id" in snake\_case format. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md index aadf9763b16049..ea13422458c7fc 100644 --- a/docs/development/core/server/kibana-plugin-server.discoveredplugin.md +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md @@ -16,7 +16,7 @@ export interface DiscoveredPlugin | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id". | +| [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id" in snake\_case format. | | [id](./kibana-plugin-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | | [optionalPlugins](./kibana-plugin-server.discoveredplugin.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | | [requiredPlugins](./kibana-plugin-server.discoveredplugin.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md index 39c1eeda47e0ec..6ffe396aa2ed1d 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md @@ -4,10 +4,15 @@ ## PluginManifest.configPath property -Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id". +Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id" in snake\_case format. Signature: ```typescript readonly configPath: ConfigPath; ``` + +## Example + +id: myPlugin configPath: my\_plugin + diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md index 44e61f11fa215b..104046f3ce7d0c 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md @@ -4,7 +4,7 @@ ## PluginManifest.id property -Identifier of the plugin. +Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.md index 9bb208a809b22d..c39a702389fb3f 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.md @@ -20,8 +20,8 @@ Should never be used in code outside of Core but is exported for documentation p | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-server.pluginmanifest.configpath.md) | ConfigPath | Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id". | -| [id](./kibana-plugin-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. | +| [configPath](./kibana-plugin-server.pluginmanifest.configpath.md) | ConfigPath | Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id" in snake\_case format. | +| [id](./kibana-plugin-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. | | [kibanaVersion](./kibana-plugin-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | | [optionalPlugins](./kibana-plugin-server.pluginmanifest.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | | [requiredPlugins](./kibana-plugin-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | diff --git a/docs/discover/document-data.asciidoc b/docs/discover/document-data.asciidoc index b45a31065aa9a6..6e9218d66c1154 100644 --- a/docs/discover/document-data.asciidoc +++ b/docs/discover/document-data.asciidoc @@ -15,7 +15,7 @@ tailor the documents table to suit your needs. [horizontal] Add a field column:: -Hover over the list of *Available fields* and then click *add* next to each field you want include as a column in the table. +Hover over the list of *Available fields* and then click *add* next to each field you want to include as a column in the table. The first field you add replaces the `_source` column. Change sort order:: By default, columns are sorted by the values in the field. If a time field is configured for the current index pattern, diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index 0ad27e68f7fe94..ecb550d3ab267d 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -25,9 +25,9 @@ At the end of the trial period, the platinum features operate in a <>. You can revert to a basic license, extend the trial, or purchase a subscription. -TIP: If {security-features} are enabled, before you revert to a basic license or -install a gold or platinum license, you must configure Transport Layer Security -(TLS) in {es}. See {ref}/encrypting-communications.html[Encrypting communications]. +TIP: If {security-features} are enabled, unless you have a trial license, +you must configure Transport Layer Security (TLS) in {es}. +See {ref}/encrypting-communications.html[Encrypting communications]. {kib} and the {ref}/start-basic.html[start basic API] provide a list of all of the features that will no longer be supported if you revert to a basic license. diff --git a/docs/maps/images/extended_stats_config.png b/docs/maps/images/extended_stats_config.png new file mode 100644 index 00000000000000..018acea96852f1 Binary files /dev/null and b/docs/maps/images/extended_stats_config.png differ diff --git a/docs/maps/images/gear_icon.png b/docs/maps/images/gear_icon.png new file mode 100644 index 00000000000000..355d55dbbc37a5 Binary files /dev/null and b/docs/maps/images/gear_icon.png differ diff --git a/docs/maps/images/gs_link_icon.png b/docs/maps/images/gs_link_icon.png deleted file mode 100644 index 89866484706133..00000000000000 Binary files a/docs/maps/images/gs_link_icon.png and /dev/null differ diff --git a/docs/maps/images/quantitative_data_driven_styling.png b/docs/maps/images/quantitative_data_driven_styling.png new file mode 100644 index 00000000000000..a7852ed2020167 Binary files /dev/null and b/docs/maps/images/quantitative_data_driven_styling.png differ diff --git a/docs/maps/images/vector_style_class.png b/docs/maps/images/vector_style_class.png index 48658bda73ee81..8c685dfcf0ab60 100644 Binary files a/docs/maps/images/vector_style_class.png and b/docs/maps/images/vector_style_class.png differ diff --git a/docs/maps/images/vector_style_dynamic.png b/docs/maps/images/vector_style_dynamic.png index 90d51144c422db..aeaef412b5220d 100644 Binary files a/docs/maps/images/vector_style_dynamic.png and b/docs/maps/images/vector_style_dynamic.png differ diff --git a/docs/maps/images/vector_style_static.png b/docs/maps/images/vector_style_static.png index 6eedb8247e6ba5..47d9c3b21fcb6f 100644 Binary files a/docs/maps/images/vector_style_static.png and b/docs/maps/images/vector_style_static.png differ diff --git a/docs/maps/vector-style.asciidoc b/docs/maps/vector-style.asciidoc index 609cd712e8b137..cd5b086508ae8a 100644 --- a/docs/maps/vector-style.asciidoc +++ b/docs/maps/vector-style.asciidoc @@ -5,6 +5,7 @@ When styling a vector layer, you can customize your data by property, such as size and color. For each property, you can specify whether to use a constant or data driven value for the style. + [float] [[maps-vector-style-static]] ==== Static styling @@ -17,13 +18,41 @@ The *kibana_sample_data_logs* layer uses static styling for all properties. [role="screenshot"] image::maps/images/vector_style_static.png[] + [float] [[maps-vector-style-data-driven]] ==== Data driven styling -Use data driven styling to symbolize features from a range of numeric property values. -To enable data driven styling, click image:maps/images/gs_link_icon.png[] next to the property. -This button is only available when vector features contain numeric properties. +Use data driven styling to symbolize features by property values. +To enable data driven styling for a style property, change the selected value from *Fixed* or *Solid* to *By value*. + +The image below shows an example of data driven styling using the <> data set. +The *kibana_sample_data_logs* layer uses data driven styling for fill color and symbol size style properties. + +* The `hour_of_day` property determines the fill color for each feature based on where the value fits on a linear scale. +Light green circles symbolize documents that occur earlier in the day, and dark green circles symbolize documents that occur later in the day. + +* The `bytes` property determines the size of each symbol based on where the value fits on a linear scale. +Smaller circles symbolize documents with smaller payloads, and larger circles symbolize documents with larger payloads. + +[role="screenshot"] +image::maps/images/vector_style_dynamic.png[] + + +[float] +[[maps-vector-style-quantitative-data-driven]] +==== Quantitative data driven styling + +Quantitative data driven styling symbolizes features from a range of numeric property values. + +To ensure symbols are consistent as you pan, zoom, and filter the map, quantitative data driven styling uses {ref}/search-aggregations-metrics-extendedstats-aggregation.html[extended_stats aggregation] to retrieve statistical metadata. + +Click the gear icon image:maps/images/gear_icon.png[] to configure extended_stats. Set *Sigma* to a smaller value to minimize outliers by moving the range minimum and maximum closer to the average. Clear the *Calculate range from indices* checkbox to turn off the extended_stats aggregation request. + +NOTE: When the *Calculate range from indices* checkbox is cleared, symbols might be inconsistent as users pan, zoom, and filter the map. Without extended_stats, the range is calulated with data from the local layer. The range is recalulcated when layer data changes. + +[role="screenshot"] +image::maps/images/extended_stats_config.png[] When the property value is undefined for a feature: @@ -31,22 +60,32 @@ When the property value is undefined for a feature: * *Border width* and *Symbol size* are set to the minimum size. * *Symbol orientation* is set to 0. -When the minimum and maximum are the same and there is no range: +When the symbol range minimum and maximum are the same and there is no range: * *Fill color* and *Border color* are set to last color in the color ramp. * *Border width* and *Symbol size* are set to the maximum size. -The image below shows an example of data driven styling using the <> data set. -The *kibana_sample_data_logs* layer uses data driven styling for fill color and symbol size style properties. -* The `hour_of_day` property determines the fill color for each feature based on where the value fits on a linear scale. -Light green circles symbolize documents that occur earlier in the day, and dark green circles symbolize documents that occur later in the day. +[float] +[[maps-vector-style-qualitative-data-driven]] +==== Qualitative data driven styling -* The `bytes` property determines the size of each symbol based on where the value fits on a linear scale. -Smaller circles symbolize documents with smaller payloads, and larger circles symbolize documents with larger payloads. +Qualitative data driven styling symbolizes non-numeric properties, such as strings and IP addresses, by category. + +Qualitative data driven styling is available for the following styling properties: + +* *Fill color* +* *Border color* +* *Label color* +* *Label border color* + +Qualitative data driven styling uses a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation] to retrieve the top nine categories for the property. Feature values within the top categories are assigned a unique color. Feature values outside of the top categories are grouped into the *Other* category. A feature is assigned the *Other* category when the property value is undefined. + +The image below shows an example of quantitative data driven styling using the <> data set. +The `machine.os.keyword` property determines the color of each symbol based on category. [role="screenshot"] -image::maps/images/vector_style_dynamic.png[] +image::maps/images/quantitative_data_driven_styling.png[] [float] diff --git a/package.json b/package.json index 9d2068b02bb934..9707d3863d295d 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,8 @@ "**/react": "^16.12.0", "**/react-test-renderer": "^16.12.0", "**/deepmerge": "^4.2.2", - "**/serialize-javascript": "^2.1.1" + "**/serialize-javascript": "^2.1.1", + "**/fast-deep-equal": "^3.1.1" }, "workspaces": { "packages": [ @@ -136,6 +137,7 @@ "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/flot": "^0.0.31", "@types/json-stable-stringify": "^1.0.32", "@types/lodash.clonedeep": "^4.5.4", "@types/node-forge": "^0.9.0", @@ -346,6 +348,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", + "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", "@types/pngjs": "^3.3.2", diff --git a/packages/kbn-i18n/src/loader.ts b/packages/kbn-i18n/src/loader.ts index 2d68079735c032..21f540f588f46f 100644 --- a/packages/kbn-i18n/src/loader.ts +++ b/packages/kbn-i18n/src/loader.ts @@ -17,15 +17,13 @@ * under the License. */ -import { readFile } from 'fs'; +import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { unique } from './core/helper'; import { Translation } from './translation'; -const asyncReadFile = promisify(readFile); - const TRANSLATION_FILE_EXTENSION = '.json'; /** @@ -69,7 +67,8 @@ function getLocaleFromFileName(fullFileName: string) { * @returns */ async function loadFile(pathToFile: string): Promise { - return JSON.parse(await asyncReadFile(pathToFile, 'utf8')); + // doing this at the moment because fs is mocked in a lot of places where this would otherwise fail + return JSON.parse(await promisify(fs.readFile)(pathToFile, 'utf8')); } /** diff --git a/renovate.json5 b/renovate.json5 index 7f67fae8941104..6764ed38ba4cf0 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -281,6 +281,14 @@ '@types/file-saver', ], }, + { + groupSlug: 'flot', + groupName: 'flot related packages', + packageNames: [ + 'flot', + '@types/flot', + ], + }, { groupSlug: 'getopts', groupName: 'getopts related packages', @@ -649,6 +657,14 @@ '@types/nodemailer', ], }, + { + groupSlug: 'numeral', + groupName: 'numeral related packages', + packageNames: [ + 'numeral', + '@types/numeral', + ], + }, { groupSlug: 'object-hash', groupName: 'object-hash related packages', diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 61c5d5b076a446..dd83ab2daca827 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -38,6 +38,7 @@ my_plugin/    ├── index.ts    └── plugin.ts ``` +- [Manifest file](/docs/development/core/server/kibana-plugin-server.pluginmanifest.md) should be defined on top level. - Both `server` and `public` should have an `index.ts` and a `plugin.ts` file: - `index.ts` should only contain: - The `plugin` export diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 173d73ffab664c..f51afd35586bd7 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1131,6 +1131,7 @@ import { npStart: { core } } from 'ui/new_platform'; | Legacy Platform | New Platform | Notes | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `chrome.addBasePath` | [`core.http.basePath.prepend`](/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md) | | +| `chrome.navLinks.update` | [`core.appbase.updater`](/docs/development/core/public/kibana-plugin-public.appbase.updater_.md) | Use the `updater$` property when registering your application via `core.application.register` | | `chrome.breadcrumbs.set` | [`core.chrome.setBreadcrumbs`](/docs/development/core/public/kibana-plugin-public.chromestart.setbreadcrumbs.md) | | | `chrome.getUiSettingsClient` | [`core.uiSettings`](/docs/development/core/public/kibana-plugin-public.uisettingsclient.md) | | | `chrome.helpExtension.set` | [`core.chrome.setHelpExtension`](/docs/development/core/public/kibana-plugin-public.chromestart.sethelpextension.md) | | diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 04535039acbe70..d7964a53358efc 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -15,6 +15,7 @@ APIs to their New Platform equivalents. - [4. New Platform plugin](#4-new-platform-plugin) - [Accessing Services](#accessing-services) - [Chrome](#chrome) + - [Updating an application navlink](#updating-application-navlink) ## Configuration @@ -462,7 +463,59 @@ elsewhere. | `chrome.setVisible` | [`core.chrome.setIsVisible`](/docs/development/core/public/kibana-plugin-public.chromestart.setisvisible.md) | | | `chrome.getInjected` | [`core.injectedMetadata.getInjected`](/docs/development/core/public/kibana-plugin-public.coresetup.injectedmetadata.md) (temporary) | A temporary API is available to read injected vars provided by legacy plugins. This will be removed after [#41990](https://github.com/elastic/kibana/issues/41990) is completed. | | `chrome.setRootTemplate` / `chrome.setRootController` | -- | Use application mounting via `core.application.register` (not currently avaiable to legacy plugins). | +| `chrome.navLinks.update` | [`core.appbase.updater`](/docs/development/core/public/kibana-plugin-public.appbase.updater_.md) | Use the `updater$` property when registering your application via `core.application.register` | In most cases, the most convenient way to access these APIs will be via the [AppMountContext](/docs/development/core/public/kibana-plugin-public.appmountcontext.md) object passed to your application when your app is mounted on the page. + +### Updating an application navlink + +In the legacy platform, the navlink could be updated using `chrome.navLinks.update` + +```ts +uiModules.get('xpack/ml').run(() => { + const showAppLink = xpackInfo.get('features.ml.showLinks', false); + const isAvailable = xpackInfo.get('features.ml.isAvailable', false); + + const navLinkUpdates = { + // hide by default, only show once the xpackInfo is initialized + hidden: !showAppLink, + disabled: !showAppLink || (showAppLink && !isAvailable), + }; + + npStart.core.chrome.navLinks.update('ml', navLinkUpdates); +}); +``` + +In the new platform, navlinks should not be updated directly. Instead, it is now possible to add an `updater` when +registering an application to change the application or the navlink state at runtime. + +```ts +// my_plugin has a required dependencie to the `licensing` plugin +interface MyPluginSetupDeps { + licensing: LicensingPluginSetup; +} + +export class MyPlugin implements Plugin { + setup({ application }, { licensing }: MyPluginSetupDeps) { + const updater$ = licensing.license$.pipe( + map(license => { + const { hidden, disabled } = calcStatusFor(license); + if (hidden) return { navLinkStatus: AppNavLinkStatus.hidden }; + if (disabled) return { navLinkStatus: AppNavLinkStatus.disabled }; + return { navLinkStatus: AppNavLinkStatus.default }; + }) + ); + + application.register({ + id: 'my-app', + title: 'My App', + updater$, + async mount(params) { + const { renderApp } = await import('./application'); + return renderApp(params); + }, + }); + } +``` \ No newline at end of file diff --git a/src/core/public/application/__snapshots__/application_service.test.ts.snap b/src/core/public/application/__snapshots__/application_service.test.ts.snap new file mode 100644 index 00000000000000..376b320b64ea9a --- /dev/null +++ b/src/core/public/application/__snapshots__/application_service.test.ts.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#start() getComponent returns renderable JSX tree 1`] = ` + +`; diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 4672a42c9eb060..54489fbd182b47 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -525,17 +525,7 @@ describe('#start()', () => { const { getComponent } = await service.start(startDeps); expect(() => shallow(createElement(getComponent))).not.toThrow(); - expect(getComponent()).toMatchInlineSnapshot(` - - `); + expect(getComponent()).toMatchSnapshot(); }); it('renders null when in legacy mode', async () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index c69b96274aa95e..4d714c8f9dad2d 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { map, shareReplay, takeUntil } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; import { InjectedMetadataSetup } from '../injected_metadata'; @@ -256,6 +256,11 @@ export class ApplicationService { ) .subscribe(apps => applications$.next(apps)); + const applicationStatuses$ = applications$.pipe( + map(apps => new Map([...apps.entries()].map(([id, app]) => [id, app.status!]))), + shareReplay(1) + ); + return { applications$, capabilities, @@ -264,11 +269,6 @@ export class ApplicationService { getUrlForApp: (appId, { path }: { path?: string } = {}) => getAppUrl(availableMounters, appId, path), navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { - const app = applications$.value.get(appId); - if (app && app.status !== AppStatus.accessible) { - // should probably redirect to the error page instead - throw new Error(`Trying to navigate to an inaccessible application: ${appId}`); - } if (await this.shouldNavigate(overlays)) { this.appLeaveHandlers.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state); @@ -283,6 +283,7 @@ export class ApplicationService { ); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index cc71cf8722df4d..4d83ab67810afa 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -18,15 +18,18 @@ */ import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { createMemoryHistory, History, createHashHistory } from 'history'; import { AppRouter, AppNotFound } from '../ui'; import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types'; import { createRenderer, createAppMounter, createLegacyAppMounter } from './utils'; +import { AppStatus } from '../types'; describe('AppContainer', () => { let mounters: MockedMounterMap; let history: History; + let appStatuses$: BehaviorSubject>; let update: ReturnType; const navigate = (path: string) => { @@ -38,6 +41,17 @@ describe('AppContainer', () => { new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter])); const setAppLeaveHandlerMock = () => undefined; + const mountersToAppStatus$ = () => { + return new BehaviorSubject( + new Map( + [...mounters.keys()].map(id => [ + id, + id.startsWith('disabled') ? AppStatus.inaccessible : AppStatus.accessible, + ]) + ) + ); + }; + beforeEach(() => { mounters = new Map([ createAppMounter('app1', 'App 1'), @@ -45,12 +59,16 @@ describe('AppContainer', () => { createAppMounter('app2', '
App 2
'), createLegacyAppMounter('baseApp:legacyApp2', jest.fn()), createAppMounter('app3', '
App 3
', '/custom/path'), + createAppMounter('disabledApp', '
Disabled app
'), + createLegacyAppMounter('disabledLegacyApp', jest.fn()), ] as Array>); history = createMemoryHistory(); + appStatuses$ = mountersToAppStatus$(); update = createRenderer( ); @@ -89,6 +107,7 @@ describe('AppContainer', () => { ); @@ -107,6 +126,7 @@ describe('AppContainer', () => { ); @@ -147,6 +167,7 @@ describe('AppContainer', () => { ); @@ -202,4 +223,16 @@ describe('AppContainer', () => { expect(dom?.exists(AppNotFound)).toBe(true); }); + + it('displays error page if app is inaccessible', async () => { + const dom = await navigate('/app/disabledApp'); + + expect(dom?.exists(AppNotFound)).toBe(true); + }); + + it('displays error page if legacy app is inaccessible', async () => { + const dom = await navigate('/app/disabledLegacyApp'); + + expect(dom?.exists(AppNotFound)).toBe(true); + }); }); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 0d955482d2226c..63e542b0127ed1 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -31,6 +31,7 @@ import { PluginOpaqueId } from '../plugins'; import { IUiSettingsClient } from '../ui_settings'; import { RecursiveReadonly } from '../../utils'; import { SavedObjectsStart } from '../saved_objects'; +import { AppCategory } from '../../types'; /** @public */ export interface AppBase { @@ -44,6 +45,13 @@ export interface AppBase { */ title: string; + /** + * The category definition of the product + * See {@link AppCategory} + * See DEFAULT_APP_CATEGORIES for more reference + */ + category?: AppCategory; + /** * The initial status of the application. * Defaulting to `accessible` diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 8afd4d0ca05514..6a630608b2c205 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -26,12 +26,13 @@ import React, { MutableRefObject, } from 'react'; -import { AppUnmount, Mounter, AppLeaveHandler } from '../types'; +import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; interface Props { appId: string; mounter?: Mounter; + appStatus: AppStatus; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; } @@ -39,10 +40,12 @@ export const AppContainer: FunctionComponent = ({ mounter, appId, setAppLeaveHandler, + appStatus, }: Props) => { const [appNotFound, setAppNotFound] = useState(false); const elementRef = useRef(null); const unmountRef: MutableRefObject = useRef(null); + // const appStatus = useObservable(appStatus$); useLayoutEffect(() => { const unmount = () => { @@ -52,7 +55,7 @@ export const AppContainer: FunctionComponent = ({ } }; const mount = async () => { - if (!mounter) { + if (!mounter || appStatus !== AppStatus.accessible) { return setAppNotFound(true); } @@ -71,7 +74,7 @@ export const AppContainer: FunctionComponent = ({ mount(); return unmount; - }, [appId, mounter, setAppLeaveHandler]); + }, [appId, appStatus, mounter, setAppLeaveHandler]); return ( diff --git a/src/core/public/application/ui/app_not_found_screen.tsx b/src/core/public/application/ui/app_not_found_screen.tsx index 73a999c5dbf162..0d651ee0480960 100644 --- a/src/core/public/application/ui/app_not_found_screen.tsx +++ b/src/core/public/application/ui/app_not_found_screen.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; export const AppNotFound = () => ( - + ; history: History; + appStatuses$: Observable>; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; } @@ -34,45 +37,59 @@ interface Params { appId: string; } -export const AppRouter: FunctionComponent = ({ history, mounters, setAppLeaveHandler }) => ( - - - {[...mounters].flatMap(([appId, mounter]) => - // Remove /app paths from the routes as they will be handled by the - // "named" route parameter `:appId` below - mounter.appBasePath.startsWith('/app') - ? [] - : [ - ( - - )} - />, - ] - )} - ) => { - // Find the mounter including legacy mounters with subapps: - const [id, mounter] = mounters.has(appId) - ? [appId, mounters.get(appId)] - : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? []; +export const AppRouter: FunctionComponent = ({ + history, + mounters, + setAppLeaveHandler, + appStatuses$, +}) => { + const appStatuses = useObservable(appStatuses$, new Map()); + return ( + + + {[...mounters].flatMap(([appId, mounter]) => + // Remove /app paths from the routes as they will be handled by the + // "named" route parameter `:appId` below + mounter.appBasePath.startsWith('/app') + ? [] + : [ + ( + + )} + />, + ] + )} + ) => { + // Find the mounter including legacy mounters with subapps: + const [id, mounter] = mounters.has(appId) + ? [appId, mounters.get(appId)] + : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? []; - return ( - - ); - }} - /> - - -); + return ( + + ); + }} + /> + + + ); +}; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index abd04722a49f20..9018b219736345 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -29,6 +29,7 @@ import { notificationServiceMock } from '../notifications/notifications_service. import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { ChromeService } from './chrome_service'; import { App } from '../application'; +import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; class FakeApp implements App { public title = `${this.id} App`; @@ -51,6 +52,7 @@ function defaultStartDeps(availableApps?: App[]) { http: httpServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), }; if (availableApps) { diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 09ea1afe35766c..6ab9fe158742a6 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -38,7 +38,7 @@ import { LoadingIndicator, HeaderWrapper as Header } from './ui'; import { DocLinksStart } from '../doc_links'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; - +import { IUiSettingsClient } from '../ui_settings'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; @@ -85,6 +85,7 @@ interface StartDeps { http: HttpStart; injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; + uiSettings: IUiSettingsClient; } /** @internal */ @@ -139,6 +140,7 @@ export class ChromeService { http, injectedMetadata, notifications, + uiSettings, }: StartDeps): Promise { this.initVisibility(application); @@ -173,7 +175,6 @@ export class ChromeService { getHeaderComponent: () => ( -
), diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 3b16c030ddcc93..4d3a1e9ecd1991 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -18,6 +18,7 @@ */ import { pick } from '../../../utils'; +import { AppCategory } from '../../'; /** * @public @@ -33,6 +34,11 @@ export interface ChromeNavLink { */ readonly title: string; + /** + * The category the app lives in + */ + readonly category?: AppCategory; + /** * The base route used to open the root of an application. */ diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index 650ef77b6fe42e..fec9322b0d77d7 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -72,6 +72,10 @@ export interface ChromeNavLinks { /** * Update the navlink for the given id with the updated attributes. * Returns the updated navlink or `undefined` if it does not exist. + * + * @deprecated Uses the {@link AppBase.updater$} property when registering + * your application with {@link ApplicationSetup.register} instead. + * * @param id * @param values */ diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap new file mode 100644 index 00000000000000..0ebc44ba67862a --- /dev/null +++ b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap @@ -0,0 +1,5224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` + + + + + + + +`; + +exports[`NavDrawer Advanced setting set to grouped renders individual items if there are less than 7 1`] = ` + + + + + + + +`; + +exports[`NavDrawer Advanced setting set to grouped renders individual items if there is only 1 category 1`] = ` + + + + + + + +`; + +exports[`NavDrawer Advanced setting set to individual renders individual items 1`] = ` + + + + + + + +`; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index d05a6bb53405c5..c3cefd180b16f6 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -17,141 +17,40 @@ * under the License. */ -import Url from 'url'; - -import React, { Component, createRef } from 'react'; -import * as Rx from 'rxjs'; - import { - // TODO: add type annotations EuiHeader, - EuiHeaderLogo, EuiHeaderSection, EuiHeaderSectionItem, EuiHeaderSectionItemButton, - EuiHorizontalRule, EuiIcon, - EuiImage, // @ts-ignore EuiNavDrawer, // @ts-ignore - EuiNavDrawerGroup, - // @ts-ignore EuiShowFor, } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; - -import { HeaderBadge } from './header_badge'; -import { HeaderBreadcrumbs } from './header_breadcrumbs'; -import { HeaderHelpMenu } from './header_help_menu'; -import { HeaderNavControls } from './header_nav_controls'; - +import React, { Component, createRef } from 'react'; +import * as Rx from 'rxjs'; import { ChromeBadge, ChromeBreadcrumb, + ChromeNavControl, ChromeNavLink, ChromeRecentlyAccessedHistoryItem, - ChromeNavControl, } from '../..'; +import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; -import { InternalApplicationStart } from '../../../application/types'; - -// Providing a buffer between the limit and the cut off index -// protects from truncating just the last couple (6) characters -const TRUNCATE_LIMIT: number = 64; -const TRUNCATE_AT: number = 58; - -/** - * - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} - -function extendRecentlyAccessedHistoryItem( - navLinks: ChromeNavLink[], - recentlyAccessed: ChromeRecentlyAccessedHistoryItem, - basePath: HttpStart['basePath'] -) { - const href = relativeToAbsolute(basePath.prepend(recentlyAccessed.link)); - const navLink = navLinks.find(nl => href.startsWith(nl.subUrlBase || nl.baseUrl)); - - let titleAndAriaLabel = recentlyAccessed.label; - if (navLink) { - const objectTypeForAriaAppendix = navLink.title; - titleAndAriaLabel = i18n.translate('core.ui.recentLinks.linkItem.screenReaderLabel', { - defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}', - values: { - recentlyAccessedItemLinklabel: recentlyAccessed.label, - pageType: objectTypeForAriaAppendix, - }, - }); - } - - return { - ...recentlyAccessed, - href, - euiIconType: navLink ? navLink.euiIconType : undefined, - title: titleAndAriaLabel, - }; -} - -function extendNavLink(navLink: ChromeNavLink) { - if (navLink.legacy) { - return { - ...navLink, - href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl, - }; - } - - return { - ...navLink, - href: navLink.baseUrl, - }; -} - -function isModifiedEvent(event: MouseEvent) { - return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); -} - -function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { - let current = element; - while (current) { - if (current.tagName === 'A') { - return current as HTMLAnchorElement; - } - - if (!current.parentElement || current.parentElement === document.body) { - return undefined; - } - - current = current.parentElement; - } -} - -function truncateRecentItemLabel(label: string): string { - if (label.length > TRUNCATE_LIMIT) { - label = `${label.substring(0, TRUNCATE_AT)}…`; - } - - return label; -} - -export type HeaderProps = Pick>; +import { HeaderBadge } from './header_badge'; +import { NavSetting, OnIsLockedUpdate } from './'; +import { HeaderBreadcrumbs } from './header_breadcrumbs'; +import { HeaderHelpMenu } from './header_help_menu'; +import { HeaderNavControls } from './header_nav_controls'; +import { euiNavLink } from './nav_link'; +import { HeaderLogo } from './header_logo'; +import { NavDrawer } from './nav_drawer'; -interface Props { +export interface HeaderProps { kibanaVersion: string; application: InternalApplicationStart; appTitle$: Rx.Observable; @@ -168,28 +67,29 @@ interface Props { legacyMode: boolean; navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; - intl: InjectedIntl; basePath: HttpStart['basePath']; isLocked?: boolean; - onIsLockedUpdate?: (isLocked: boolean) => void; + navSetting$: Rx.Observable; + onIsLockedUpdate?: OnIsLockedUpdate; } interface State { appTitle: string; - currentAppId?: string; isVisible: boolean; - navLinks: ReadonlyArray>; - recentlyAccessed: ReadonlyArray>; + navLinks: ChromeNavLink[]; + recentlyAccessed: ChromeRecentlyAccessedHistoryItem[]; forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; + navSetting: NavSetting; + currentAppId: string | undefined; } -class HeaderUI extends Component { +export class Header extends Component { private subscription?: Rx.Subscription; private navDrawerRef = createRef(); - constructor(props: Props) { + constructor(props: HeaderProps) { super(props); this.state = { @@ -200,6 +100,8 @@ class HeaderUI extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], + navSetting: 'grouped', + currentAppId: '', }; } @@ -214,7 +116,8 @@ class HeaderUI extends Component { Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$ + this.props.application.currentAppId$, + this.props.navSetting$ ) ).subscribe({ next: ([ @@ -223,18 +126,17 @@ class HeaderUI extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId], + [navControlsLeft, navControlsRight, currentAppId, navSetting], ]) => { this.setState({ appTitle, isVisible, forceNavigation, - navLinks: navLinks.map(extendNavLink), - recentlyAccessed: recentlyAccessed.map(ra => - extendRecentlyAccessedHistoryItem(navLinks, ra, this.props.basePath) - ), + navLinks: navLinks.filter(navLink => !navLink.hidden), + recentlyAccessed, navControlsLeft, navControlsRight, + navSetting, currentAppId, }); }, @@ -247,26 +149,12 @@ class HeaderUI extends Component { } } - public renderLogo() { - const { homeHref, intl } = this.props; - return ( - - ); - } - public renderMenuTrigger() { return ( this.navDrawerRef.current.toggleOpen()} > @@ -275,98 +163,29 @@ class HeaderUI extends Component { } public render() { + const { appTitle, isVisible, navControlsLeft, navControlsRight } = this.state; const { - application, badge$, - basePath, breadcrumbs$, helpExtension$, helpSupportUrl$, - intl, - isLocked, kibanaDocLink, kibanaVersion, - onIsLockedUpdate, - legacyMode, } = this.props; - const { - appTitle, - currentAppId, - isVisible, - navControlsLeft, - navControlsRight, - navLinks, - recentlyAccessed, - } = this.state; + const navLinks = this.state.navLinks.map(link => + euiNavLink( + link, + this.props.legacyMode, + this.state.currentAppId, + this.props.basePath, + this.props.application.navigateToApp + ) + ); if (!isVisible) { return null; } - const navLinksArray = navLinks - .filter(navLink => !navLink.hidden) - .map(navLink => ({ - key: navLink.id, - label: navLink.tooltip ?? navLink.title, - - // Use href and onClick to support "open in new tab" and SPA navigation in the same link - href: navLink.href, - onClick: (event: MouseEvent) => { - if ( - !legacyMode && // ignore when in legacy mode - !navLink.legacy && // ignore links to legacy apps - !event.defaultPrevented && // onClick prevented default - event.button === 0 && // ignore everything but left clicks - !isModifiedEvent(event) // ignore clicks with modifier keys - ) { - event.preventDefault(); - application.navigateToApp(navLink.id); - } - }, - - // Legacy apps use `active` property, NP apps should match the current app - isActive: navLink.active || currentAppId === navLink.id, - isDisabled: navLink.disabled, - - iconType: navLink.euiIconType, - icon: - !navLink.euiIconType && navLink.icon ? ( - - ) : ( - undefined - ), - 'data-test-subj': 'navDrawerAppsMenuLink', - })); - - const recentLinksArray = [ - { - label: intl.formatMessage({ - id: 'core.ui.chrome.sideGlobalNav.viewRecentItemsLabel', - defaultMessage: 'Recently viewed', - }), - iconType: 'clock', - isDisabled: recentlyAccessed.length > 0 ? false : true, - flyoutMenu: { - title: intl.formatMessage({ - id: 'core.ui.chrome.sideGlobalNav.viewRecentItemsFlyoutTitle', - defaultMessage: 'Recent items', - }), - listItems: recentlyAccessed.map(item => ({ - label: truncateRecentItemLabel(item.label), - title: item.title, - 'aria-label': item.title, - href: item.href, - iconType: item.euiIconType, - })), - }, - }, - ]; - return (
@@ -375,7 +194,13 @@ class HeaderUI extends Component { {this.renderMenuTrigger()} - {this.renderLogo()} + + + @@ -399,75 +224,17 @@ class HeaderUI extends Component { - - - - - - + />
); } - - private onNavClick = (event: React.MouseEvent) => { - const anchor = findClosestAnchor((event as any).nativeEvent.target); - if (!anchor) { - return; - } - - const navLink = this.state.navLinks.find(item => item.href === anchor.href); - if (navLink && navLink.disabled) { - event.preventDefault(); - return; - } - - if ( - !this.state.forceNavigation || - event.isDefaultPrevented() || - event.altKey || - event.metaKey || - event.ctrlKey - ) { - return; - } - - const toParsed = Url.parse(anchor.href); - const fromParsed = Url.parse(document.location.href); - const sameProto = toParsed.protocol === fromParsed.protocol; - const sameHost = toParsed.host === fromParsed.host; - const samePath = toParsed.path === fromParsed.path; - - if (sameProto && sameHost && samePath) { - if (toParsed.hash) { - document.location.reload(); - } - - // event.preventDefault() keeps the browser from seeing the new url as an update - // and even setting window.location does not mimic that behavior, so instead - // we use stopPropagation() to prevent angular from seeing the click and - // starting a digest cycle/attempting to handle it in the router. - event.stopPropagation(); - } - }; } - -export const Header = injectI18n(HeaderUI); diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx new file mode 100644 index 00000000000000..793b8646dabf78 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -0,0 +1,104 @@ +/* + * 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 Url from 'url'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiHeaderLogo } from '@elastic/eui'; +import { NavLink } from './nav_link'; + +function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { + let current = element; + while (current) { + if (current.tagName === 'A') { + return current as HTMLAnchorElement; + } + + if (!current.parentElement || current.parentElement === document.body) { + return undefined; + } + + current = current.parentElement; + } +} + +function onClick( + event: React.MouseEvent, + forceNavigation: boolean, + navLinks: NavLink[] +) { + const anchor = findClosestAnchor((event as any).nativeEvent.target); + if (!anchor) { + return; + } + + const navLink = navLinks.find(item => item.href === anchor.href); + if (navLink && navLink.isDisabled) { + event.preventDefault(); + return; + } + + if ( + !forceNavigation || + event.isDefaultPrevented() || + event.altKey || + event.metaKey || + event.ctrlKey + ) { + return; + } + + const toParsed = Url.parse(anchor.href); + const fromParsed = Url.parse(document.location.href); + const sameProto = toParsed.protocol === fromParsed.protocol; + const sameHost = toParsed.host === fromParsed.host; + const samePath = toParsed.path === fromParsed.path; + + if (sameProto && sameHost && samePath) { + if (toParsed.hash) { + document.location.reload(); + } + + // event.preventDefault() keeps the browser from seeing the new url as an update + // and even setting window.location does not mimic that behavior, so instead + // we use stopPropagation() to prevent angular from seeing the click and + // starting a digest cycle/attempting to handle it in the router. + event.stopPropagation(); + } +} + +interface Props { + href: string; + navLinks: NavLink[]; + forceNavigation: boolean; +} + +export function HeaderLogo({ href, forceNavigation, navLinks }: Props) { + return ( + onClick(e, forceNavigation, navLinks)} + href={href} + aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { + defaultMessage: 'Go to home page', + })} + /> + ); +} diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index 6d59fc6d9433b8..b396c94b3f2a3e 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -26,3 +26,5 @@ export { ChromeHelpExtensionMenuDocumentationLink, ChromeHelpExtensionMenuGitHubLink, } from './header_help_menu'; +export type NavSetting = 'grouped' | 'individual'; +export type OnIsLockedUpdate = (isLocked: boolean) => void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.test.tsx b/src/core/public/chrome/ui/header/nav_drawer.test.tsx new file mode 100644 index 00000000000000..7272935b93a520 --- /dev/null +++ b/src/core/public/chrome/ui/header/nav_drawer.test.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cloneDeep } from 'lodash'; +import { mount } from 'enzyme'; +import React from 'react'; +import { NavSetting } from './'; +import { ChromeNavLink } from '../../../'; +import { AppCategory } from 'src/core/types'; +import { DEFAULT_APP_CATEGORIES } from '../../../../utils'; +import { NavDrawer } from './nav_drawer'; +import { euiNavLink } from './nav_link'; + +const { analyze, management, observability, security } = DEFAULT_APP_CATEGORIES; +const mockIBasePath = { + get: () => '/app', + prepend: () => '/app', + remove: () => '/app', +}; + +const getMockProps = (chromeNavLinks: ChromeNavLink[], navSetting: NavSetting = 'grouped') => ({ + navSetting, + navLinks: chromeNavLinks.map(link => + euiNavLink(link, true, undefined, mockIBasePath, () => Promise.resolve()) + ), + chromeNavLinks, + recentlyAccessedItems: [], + basePath: mockIBasePath, +}); + +const makeLink = (id: string, order: number, category?: AppCategory) => ({ + id, + category, + order, + title: id, + baseUrl: `http://localhost:5601/app/${id}`, + legacy: true, +}); + +const getMockChromeNavLink = () => + cloneDeep([ + makeLink('discover', 100, analyze), + makeLink('siem', 500, security), + makeLink('metrics', 600, observability), + makeLink('monitoring', 800, management), + makeLink('visualize', 200, analyze), + makeLink('dashboard', 300, analyze), + makeLink('canvas', 400, { label: 'customCategory' }), + makeLink('logs', 700, observability), + ]); + +describe('NavDrawer', () => { + describe('Advanced setting set to individual', () => { + it('renders individual items', () => { + const component = mount( + + ); + expect(component).toMatchSnapshot(); + }); + }); + describe('Advanced setting set to grouped', () => { + it('renders individual items if there are less than 7', () => { + const links = getMockChromeNavLink().slice(0, 5); + const component = mount(); + expect(component).toMatchSnapshot(); + }); + it('renders individual items if there is only 1 category', () => { + // management doesn't count as a category + const navLinks = [ + makeLink('discover', 100, analyze), + makeLink('siem', 500, analyze), + makeLink('metrics', 600, analyze), + makeLink('monitoring', 800, analyze), + makeLink('visualize', 200, analyze), + makeLink('dashboard', 300, management), + makeLink('canvas', 400, management), + makeLink('logs', 700, management), + ]; + const component = mount(); + expect(component).toMatchSnapshot(); + }); + it('renders grouped items', () => { + const component = mount(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx new file mode 100644 index 00000000000000..dbb68d5dd3901e --- /dev/null +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -0,0 +1,170 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { groupBy, sortBy } from 'lodash'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; +import { NavSetting, OnIsLockedUpdate } from './'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; +import { AppCategory } from '../../../../types'; +import { HttpStart } from '../../../http'; +import { NavLink } from './nav_link'; +import { RecentLinks } from './recent_links'; + +function getAllCategories(allCategorizedLinks: Record) { + const allCategories = {} as Record; + + for (const [key, value] of Object.entries(allCategorizedLinks)) { + allCategories[key] = value[0].category; + } + + return allCategories; +} + +function getOrderedCategories( + mainCategories: Record, + categoryDictionary: ReturnType +) { + return sortBy( + Object.keys(mainCategories), + categoryName => categoryDictionary[categoryName]?.order + ); +} + +export interface Props { + navSetting: NavSetting; + isLocked?: boolean; + onIsLockedUpdate?: OnIsLockedUpdate; + navLinks: NavLink[]; + chromeNavLinks: ChromeNavLink[]; + recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[]; + basePath: HttpStart['basePath']; +} + +function navDrawerRenderer( + { + navSetting, + isLocked, + onIsLockedUpdate, + navLinks, + chromeNavLinks, + recentlyAccessedItems, + basePath, + }: Props, + ref: React.Ref +) { + const disableGroupedNavSetting = navSetting === 'individual'; + const groupedNavLinks = groupBy(navLinks, link => link?.category?.label); + const { undefined: unknowns, ...allCategorizedLinks } = groupedNavLinks; + const { Management: management, ...mainCategories } = allCategorizedLinks; + const categoryDictionary = getAllCategories(allCategorizedLinks); + const orderedCategories = getOrderedCategories(mainCategories, categoryDictionary); + const showUngroupedNav = + disableGroupedNavSetting || navLinks.length < 7 || Object.keys(mainCategories).length === 1; + + return ( + + {RecentLinks({ + recentlyAccessedItems, + navLinks: chromeNavLinks, + basePath, + })} + + {showUngroupedNav ? ( + + ) : ( + <> + { + const category = categoryDictionary[categoryName]!; + const links = mainCategories[categoryName]; + + if (links.length === 1) { + return { + ...links[0], + label: category.label, + iconType: category.euiIconType || links[0].iconType, + }; + } + + return { + 'data-test-subj': 'navDrawerCategory', + iconType: category.euiIconType, + label: category.label, + flyoutMenu: { + title: category.label, + listItems: sortBy(links, 'order').map(link => { + link['data-test-subj'] = 'navDrawerFlyoutLink'; + return link; + }), + }, + }; + }), + ...sortBy(unknowns, 'order'), + ]} + /> + + { + link['data-test-subj'] = 'navDrawerFlyoutLink'; + return link; + }), + }, + }, + ]} + /> + + )} + + ); +} + +export const NavDrawer = React.forwardRef(navDrawerRenderer); diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx new file mode 100644 index 00000000000000..52b59c53b658c0 --- /dev/null +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiImage } from '@elastic/eui'; +import { ChromeNavLink, CoreStart } from '../../../'; +import { HttpStart } from '../../../http'; + +function isModifiedEvent(event: MouseEvent) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); +} + +function LinkIcon({ url }: { url: string }) { + return ; +} + +export type NavLink = ReturnType; + +export function euiNavLink( + navLink: ChromeNavLink, + legacyMode: boolean, + currentAppId: string | undefined, + basePath: HttpStart['basePath'], + navigateToApp: CoreStart['application']['navigateToApp'] +) { + const { + legacy, + url, + active, + baseUrl, + id, + title, + disabled, + euiIconType, + icon, + category, + order, + tooltip, + } = navLink; + let href = navLink.baseUrl; + + if (legacy) { + href = url && !active ? url : baseUrl; + } + + return { + category, + key: id, + label: tooltip ?? title, + href, // Use href and onClick to support "open in new tab" and SPA navigation in the same link + onClick(event: MouseEvent) { + if ( + !legacyMode && // ignore when in legacy mode + !legacy && // ignore links to legacy apps + !event.defaultPrevented && // onClick prevented default + event.button === 0 && // ignore everything but left clicks + !isModifiedEvent(event) // ignore clicks with modifier keys + ) { + event.preventDefault(); + navigateToApp(navLink.id); + } + }, + // Legacy apps use `active` property, NP apps should match the current app + isActive: active || currentAppId === id, + isDisabled: disabled, + iconType: euiIconType, + icon: !euiIconType && icon ? : undefined, + order, + 'data-test-subj': 'navDrawerAppsMenuLink', + }; +} diff --git a/src/core/public/chrome/ui/header/recent_links.tsx b/src/core/public/chrome/ui/header/recent_links.tsx new file mode 100644 index 00000000000000..a947ab1c450563 --- /dev/null +++ b/src/core/public/chrome/ui/header/recent_links.tsx @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { EuiNavDrawerGroup } from '@elastic/eui'; +import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; +import { HttpStart } from '../../../http'; + +// Providing a buffer between the limit and the cut off index +// protects from truncating just the last couple (6) characters +const TRUNCATE_LIMIT: number = 64; +const TRUNCATE_AT: number = 58; + +export function truncateRecentItemLabel(label: string): string { + if (label.length > TRUNCATE_LIMIT) { + label = `${label.substring(0, TRUNCATE_AT)}…`; + } + + return label; +} + +/** + * @param {string} url - a relative or root relative url. If a relative path is given then the + * absolute url returned will depend on the current page where this function is called from. For example + * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get + * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that + * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". + * @return {string} the relative url transformed into an absolute url + */ +function relativeToAbsolute(url: string) { + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} + +function prepareForEUI( + recentlyAccessed: ChromeRecentlyAccessedHistoryItem[], + navLinks: ChromeNavLink[], + basePath: HttpStart['basePath'] +) { + return recentlyAccessed.map(({ link, label }) => { + const href = relativeToAbsolute(basePath.prepend(link)); + const navLink = navLinks.find(nl => href.startsWith(nl.baseUrl ?? nl.subUrlBase)); + let titleAndAriaLabel = label; + + if (navLink) { + titleAndAriaLabel = i18n.translate('core.ui.recentLinks.linkItem.screenReaderLabel', { + defaultMessage: '{recentlyAccessedItemLinklabel}, type: {pageType}', + values: { + recentlyAccessedItemLinklabel: label, + pageType: navLink.title, + }, + }); + } + + return { + href, + label: truncateRecentItemLabel(label), + title: titleAndAriaLabel, + 'aria-label': titleAndAriaLabel, + iconType: navLink?.euiIconType, + }; + }); +} + +interface Props { + recentlyAccessedItems: ChromeRecentlyAccessedHistoryItem[]; + navLinks: ChromeNavLink[]; + basePath: HttpStart['basePath']; +} + +export function RecentLinks({ recentlyAccessedItems, navLinks, basePath }: Props) { + return ( + + ); +} diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 1ee41fe64418ec..94fa74f4bd861f 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -429,15 +429,14 @@ describe('Notifications targetDomElement', () => { rootDomElement, }); - let targetDomElementParentInStart: HTMLElement | null; + let targetDomElementInStart: HTMLElement | null; MockNotificationsService.start.mockImplementation(({ targetDomElement }): any => { - expect(targetDomElement.parentElement).not.toBeNull(); - targetDomElementParentInStart = targetDomElement.parentElement; + targetDomElementInStart = targetDomElement; }); // Starting the core system should pass the targetDomElement as a child of the rootDomElement await core.setup(); await core.start(); - expect(targetDomElementParentInStart!).toBe(rootDomElement); + expect(targetDomElementInStart!.parentElement).toBe(rootDomElement); }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 5b31c740518e4a..5fb12ec1549521 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -214,6 +214,7 @@ export class CoreSystem { const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); + const fatalErrors = await this.fatalErrors.start(); await this.integrations.start({ uiSettings }); const coreUiTargetDomElement = document.createElement('div'); @@ -221,13 +222,6 @@ export class CoreSystem { const notificationsTargetDomElement = document.createElement('div'); const overlayTargetDomElement = document.createElement('div'); - // ensure the rootDomElement is empty - this.rootDomElement.textContent = ''; - this.rootDomElement.classList.add('coreSystemRootDomElement'); - this.rootDomElement.appendChild(coreUiTargetDomElement); - this.rootDomElement.appendChild(notificationsTargetDomElement); - this.rootDomElement.appendChild(overlayTargetDomElement); - const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement, @@ -245,6 +239,7 @@ export class CoreSystem { http, injectedMetadata, notifications, + uiSettings, }); application.registerMountContext(this.coreContext.coreId, 'core', () => ({ @@ -271,9 +266,18 @@ export class CoreSystem { notifications, overlays, uiSettings, + fatalErrors, }; const plugins = await this.plugins.start(core); + + // ensure the rootDomElement is empty + this.rootDomElement.textContent = ''; + this.rootDomElement.classList.add('coreSystemRootDomElement'); + this.rootDomElement.appendChild(coreUiTargetDomElement); + this.rootDomElement.appendChild(notificationsTargetDomElement); + this.rootDomElement.appendChild(overlayTargetDomElement); + const rendering = this.rendering.start({ application, chrome, diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index dd7702a7ee7dd9..6d9876f787fa97 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -26,18 +26,22 @@ const createSetupContractMock = () => { return setupContract; }; +const createStartContractMock = createSetupContractMock; type FatalErrorsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), }; mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const fatalErrorsServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/public/fatal_errors/fatal_errors_service.tsx b/src/core/public/fatal_errors/fatal_errors_service.tsx index 5c6a7bb322ae1f..309f07859ef264 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.tsx +++ b/src/core/public/fatal_errors/fatal_errors_service.tsx @@ -54,9 +54,18 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } +/** + * FatalErrors stop the Kibana Public Core and displays a fatal error screen + * with details about the Kibana build and the error. + * + * @public + */ +export type FatalErrorsStart = FatalErrorsSetup; + /** @interal */ export class FatalErrorsService { private readonly errorInfo$ = new Rx.ReplaySubject(); + private fatalErrors?: FatalErrorsSetup; /** * @@ -82,7 +91,7 @@ export class FatalErrorsService { }, }); - const fatalErrorsSetup: FatalErrorsSetup = { + this.fatalErrors = { add: (error, source?) => { const errorInfo = getErrorInfo(error, source); @@ -101,9 +110,17 @@ export class FatalErrorsService { }, }; - this.setupGlobalErrorHandlers(fatalErrorsSetup); + this.setupGlobalErrorHandlers(this.fatalErrors!); - return fatalErrorsSetup; + return this.fatalErrors!; + } + + public start() { + const { fatalErrors } = this; + if (!fatalErrors) { + throw new Error('FatalErrorsService#setup() must be invoked before start.'); + } + return fatalErrors; } private renderError(injectedMetadata: InjectedMetadataSetup, i18n: I18nStart) { diff --git a/src/core/public/fatal_errors/index.ts b/src/core/public/fatal_errors/index.ts index e37a36152cf91b..c8ea1c0bccd227 100644 --- a/src/core/public/fatal_errors/index.ts +++ b/src/core/public/fatal_errors/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { FatalErrorsSetup, FatalErrorsService } from './fatal_errors_service'; +export { FatalErrorsSetup, FatalErrorsStart, FatalErrorsService } from './fatal_errors_service'; export { FatalErrorInfo } from './get_error_info'; diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index b86f1f5c08029b..b7ceaed6e56a76 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -133,7 +133,11 @@ export class Fetch { try { response = await window.fetch(request); } catch (err) { - throw new HttpFetchError(err.message, request); + if (err.name === 'AbortError') { + throw err; + } else { + throw new HttpFetchError(err.message, request); + } } const contentType = response.headers.get('Content-Type') || ''; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 5b17eccc37f8b8..bf8cab9a3c7787 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -55,7 +55,7 @@ import { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, } from './chrome'; -import { FatalErrorsSetup, FatalErrorInfo } from './fatal_errors'; +import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata'; @@ -77,7 +77,8 @@ import { } from './context'; export { CoreContext, CoreSystem } from './core_system'; -export { RecursiveReadonly } from '../utils'; +export { RecursiveReadonly, DEFAULT_APP_CATEGORIES } from '../utils'; +export { AppCategory } from '../types'; export { ApplicationSetup, @@ -232,6 +233,8 @@ export interface CoreStart { overlays: OverlayStart; /** {@link IUiSettingsClient} */ uiSettings: IUiSettingsClient; + /** {@link FatalErrorsStart} */ + fatalErrors: FatalErrorsStart; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values @@ -302,6 +305,7 @@ export { DocLinksStart, FatalErrorInfo, FatalErrorsSetup, + FatalErrorsStart, HttpSetup, HttpStart, I18nStart, diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 0bde1b68e1876a..1075a7741ee324 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -26,10 +26,12 @@ import { UserProvidedValues, } from '../../server/types'; import { deepFreeze } from '../../utils/'; +import { AppCategory } from '../'; /** @public */ export interface LegacyNavLink { id: string; + category?: AppCategory; title: string; order: number; url: string; @@ -52,6 +54,7 @@ export interface InjectedMetadataParams { buildNumber: number; branch: string; basePath: string; + category?: AppCategory; csp: { warnLegacyBrowsers: boolean; }; @@ -75,6 +78,7 @@ export interface InjectedMetadataParams { basePath: string; serverName: string; devMode: boolean; + category?: AppCategory; uiSettings: { defaults: Record; user?: Record; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 9dd24f9e4a7a3c..d08c8b52e39c9b 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -98,6 +98,7 @@ const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); const savedObjectsStart = savedObjectsMock.createStartContract(); +const fatalErrorsStart = fatalErrorsServiceMock.createStartContract(); const mockStorage = { getItem: jest.fn() } as any; const defaultStartDeps = { @@ -112,6 +113,7 @@ const defaultStartDeps = { overlays: overlayStart, uiSettings: uiSettingsStart, savedObjects: savedObjectsStart, + fatalErrors: fatalErrorsStart, }, lastSubUrlStorage: mockStorage, targetDomElement: document.createElement('div'), diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index f906aff1759e2e..cc3210771eecc2 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -74,6 +74,7 @@ export class LegacyPlatformService { appUrl: navLink.url, subUrlBase: navLink.subUrlBase, linkToLastSubUrl: navLink.linkToLastSubUrl, + category: navLink.category, }) ); diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 43c8aa6f1d6b96..ce90d49065ad4e 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -74,6 +74,7 @@ function createCoreStartMock({ basePath = '' } = {}) { injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, + fatalErrors: fatalErrorsServiceMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index f146c2452868b4..48100cba4f26e0 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -151,5 +151,6 @@ export function createPluginStartContext< injectedMetadata: { getInjectedVar: deps.injectedMetadata.getInjectedVar, }, + fatalErrors: deps.fatalErrors, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index cafc7e5887e385..dbbcda8d60e128 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -111,6 +111,7 @@ describe('PluginsService', () => { overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsMock.createStartContract(), + fatalErrors: fatalErrorsServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 610b08708c6812..0da6e0d422f2d7 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -26,6 +26,7 @@ export interface App extends AppBase { // @public (undocumented) export interface AppBase { capabilities?: Partial; + category?: AppCategory; chromeless?: boolean; euiIconType?: string; icon?: string; @@ -40,6 +41,14 @@ export interface AppBase { updater$?: Observable; } +// @public +export interface AppCategory { + ariaLabel?: string; + euiIconType?: string; + label: string; + order?: number; +} + // @public export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction; @@ -251,6 +260,7 @@ export interface ChromeNavLink { // @deprecated readonly active?: boolean; readonly baseUrl: string; + readonly category?: AppCategory; // @deprecated readonly disabled?: boolean; readonly euiIconType?: string; @@ -279,6 +289,7 @@ export interface ChromeNavLinks { getNavLinks$(): Observable>>; has(id: string): boolean; showOnly(id: string): void; + // @deprecated update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; } @@ -376,6 +387,8 @@ export interface CoreStart { // (undocumented) docLinks: DocLinksStart; // (undocumented) + fatalErrors: FatalErrorsStart; + // (undocumented) http: HttpStart; // (undocumented) i18n: I18nStart; @@ -407,6 +420,26 @@ export class CoreSystem { stop(): void; } +// @internal (undocumented) +export const DEFAULT_APP_CATEGORIES: Readonly<{ + analyze: { + label: string; + order: number; + }; + observability: { + label: string; + order: number; + }; + security: { + label: string; + order: number; + }; + management: { + label: string; + euiIconType: string; + }; +}>; + // @public (undocumented) export interface DocLinksStart { // (undocumented) @@ -531,6 +564,9 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } +// @public +export type FatalErrorsStart = FatalErrorsSetup; + // @public export type HandlerContextType> = T extends HandlerFunction ? U : never; @@ -740,6 +776,8 @@ export interface LegacyCoreStart extends CoreStart { // @public (undocumented) export interface LegacyNavLink { + // (undocumented) + category?: AppCategory; // (undocumented) euiIconType?: string; // (undocumented) diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index e633e00965c6ab..0c34a16c68e99d 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -448,23 +448,4 @@ describe('SavedObjectsClient', () => { `); }); }); - - it('maintains backwards compatibility by transforming http.fetch errors to be compatible with kfetch errors', () => { - const err = { - response: { ok: false, redirected: false, status: 409, statusText: 'Conflict' }, - body: 'response body', - }; - http.fetch.mockRejectedValue(err); - return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot(` - Object { - "body": "response body", - "res": Object { - "ok": false, - "redirected": false, - "status": 409, - "statusText": "Conflict", - }, - } - `); - }); }); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index dab98ee66cdb10..ccb23793a85349 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -465,11 +465,7 @@ export class SavedObjectsClient { * uses `{response: {status: number}}`. */ private savedObjectsFetch(path: string, { method, query, body }: HttpFetchOptions) { - return this.http.fetch(path, { method, query, body }).catch(err => { - const kfetchError = Object.assign(err, { res: err.response }); - delete kfetchError.response; - return Promise.reject(kfetchError); - }); + return this.http.fetch(path, { method, query, body }); } } diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index e85f8567bfc683..5bc3887f05f931 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -36,7 +36,13 @@ describe('configuration deprecations', () => { await root.setup(); const logs = loggingServiceMock.collect(mockLoggingService); - expect(logs.warn).toMatchInlineSnapshot(`Array []`); + const warnings = logs.warn.flatMap(i => i); + expect(warnings).not.toContain( + '"optimize.lazy" is deprecated and has been replaced by "optimize.watch"' + ); + expect(warnings).not.toContain( + '"optimize.lazyPort" is deprecated and has been replaced by "optimize.watchPort"' + ); }); it('should log deprecation warnings for core deprecations', async () => { @@ -50,15 +56,12 @@ describe('configuration deprecations', () => { await root.setup(); const logs = loggingServiceMock.collect(mockLoggingService); - expect(logs.warn).toMatchInlineSnapshot(` - Array [ - Array [ - "\\"optimize.lazy\\" is deprecated and has been replaced by \\"optimize.watch\\"", - ], - Array [ - "\\"optimize.lazyPort\\" is deprecated and has been replaced by \\"optimize.watchPort\\"", - ], - ] - `); + const warnings = logs.warn.flatMap(i => i); + expect(warnings).toContain( + '"optimize.lazy" is deprecated and has been replaced by "optimize.watch"' + ); + expect(warnings).toContain( + '"optimize.lazyPort" is deprecated and has been replaced by "optimize.watchPort"' + ); }); }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 50d291b1736406..3f67b9a656bb79 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -283,7 +283,7 @@ export interface RequestHandlerContext { * * @public */ -export interface CoreSetup { +export interface CoreSetup { /** {@link CapabilitiesSetup} */ capabilities: CapabilitiesSetup; /** {@link ContextSetup} */ @@ -298,6 +298,13 @@ export interface CoreSetup { uiSettings: UiSettingsServiceSetup; /** {@link UuidServiceSetup} */ uuid: UuidServiceSetup; + /** + * Allows plugins to get access to APIs available in start inside async handlers. + * Promise will not resolve until Core and plugin dependencies have completed `start`. + * This should only be used inside handlers registered during `setup` that will only be executed + * after `start` lifecycle. + */ + getStartServices(): Promise<[CoreStart, TPluginsStart]>; } /** diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index ffcbf1662ee85e..07cc9330330545 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -256,6 +256,12 @@ export class LegacyService implements CoreService { startDeps: LegacyServiceStartDeps, legacyPlugins: LegacyPlugins ) { + const coreStart: CoreStart = { + capabilities: startDeps.core.capabilities, + savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient }, + uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, + }; + const coreSetup: CoreSetup = { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, @@ -291,11 +297,7 @@ export class LegacyService implements CoreService { uuid: { getInstanceUuid: setupDeps.core.uuid.getInstanceUuid, }, - }; - const coreStart: CoreStart = { - capabilities: startDeps.core.capabilities, - savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient }, - uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, + getStartServices: () => Promise.resolve([coreStart, startDeps.plugins]), }; // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 9867274d224bd0..a19133c30659b0 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -65,6 +65,7 @@ function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: Le return { id, + category: spec.category, title: spec.title, order: typeof spec.order === 'number' ? spec.order : 0, icon: spec.icon, @@ -79,6 +80,7 @@ function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[] return (uiExports.navLinkSpecs || []) .map(spec => ({ id: spec.id, + category: spec.category, title: spec.title, order: typeof spec.order === 'number' ? spec.order : 0, url: spec.url, diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 40b8244a318903..d51058ca561c6b 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -139,7 +139,7 @@ export type LegacyNavLinkSpec = Record & ChromeNavLink; */ export type LegacyAppSpec = Pick< ChromeNavLink, - 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden' + 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden' | 'category' > & { pluginId?: string; id?: string; listed?: boolean }; /** diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index c7082d46313ae3..c0a8973d98a548 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -86,6 +86,8 @@ function pluginInitializerContextMock(config: T = {} as T) { return mock; } +type CoreSetupMockType = MockedKeys & jest.Mocked>; + function createCoreSetupMock() { const httpService = httpServiceMock.createSetupContract(); const httpMock: jest.Mocked = { @@ -105,7 +107,7 @@ function createCoreSetupMock() { const uiSettingsMock = { register: uiSettingsServiceMock.createSetupContract().register, }; - const mock: MockedKeys = { + const mock: CoreSetupMockType = { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetup(), @@ -113,6 +115,9 @@ function createCoreSetupMock() { savedObjects: savedObjectsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), + getStartServices: jest + .fn, object]>, []>() + .mockResolvedValue([createCoreStartMock(), {}]), }; return mock; diff --git a/src/core/server/plugins/discovery/is_camel_case.test.ts b/src/core/server/plugins/discovery/is_camel_case.test.ts new file mode 100644 index 00000000000000..eb8cb8f170dac1 --- /dev/null +++ b/src/core/server/plugins/discovery/is_camel_case.test.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isCamelCase } from './is_camel_case'; + +describe('isCamelCase', () => { + it('matches a string in camelCase', () => { + expect(isCamelCase('foo')).toBe(true); + expect(isCamelCase('foo1')).toBe(true); + expect(isCamelCase('fooBar')).toBe(true); + expect(isCamelCase('fooBarBaz')).toBe(true); + expect(isCamelCase('fooBAR')).toBe(true); + }); + + it('does not match strings in other cases', () => { + expect(isCamelCase('AAA')).toBe(false); + expect(isCamelCase('FooBar')).toBe(false); + expect(isCamelCase('3Foo')).toBe(false); + expect(isCamelCase('o_O')).toBe(false); + expect(isCamelCase('foo_bar')).toBe(false); + expect(isCamelCase('foo_')).toBe(false); + expect(isCamelCase('_fooBar')).toBe(false); + expect(isCamelCase('fooBar_')).toBe(false); + expect(isCamelCase('_fooBar_')).toBe(false); + }); +}); diff --git a/src/core/server/plugins/discovery/is_camel_case.ts b/src/core/server/plugins/discovery/is_camel_case.ts new file mode 100644 index 00000000000000..12069ec473f8de --- /dev/null +++ b/src/core/server/plugins/discovery/is_camel_case.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +const camelCaseRegExp = /^[a-z]{1}([a-zA-Z0-9]{1,})$/; +export function isCamelCase(candidate: string) { + return camelCaseRegExp.test(candidate); +} diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts index 17b1ac7b860452..979accb1f769ef 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts @@ -20,10 +20,12 @@ import { PluginDiscoveryErrorType } from './plugin_discovery_error'; import { mockReadFile } from './plugin_manifest_parser.test.mocks'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; import { resolve } from 'path'; import { parseManifest } from './plugin_manifest_parser'; +const logger = loggingServiceMock.createLogger(); const pluginPath = resolve('path', 'existent-dir'); const pluginManifestPath = resolve(pluginPath, 'kibana.json'); const packageInfo = { @@ -43,7 +45,7 @@ test('return error when manifest is empty', async () => { cb(null, Buffer.from('')); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Unexpected end of JSON input (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -55,7 +57,7 @@ test('return error when manifest content is null', async () => { cb(null, Buffer.from('null')); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Plugin manifest must contain a JSON encoded object. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -67,7 +69,7 @@ test('return error when manifest content is not a valid JSON', async () => { cb(null, Buffer.from('not-json')); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Unexpected token o in JSON at position 1 (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -79,7 +81,7 @@ test('return error when plugin id is missing', async () => { cb(null, Buffer.from(JSON.stringify({ version: 'some-version' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Plugin manifest must contain an "id" property. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -91,20 +93,36 @@ test('return error when plugin id includes `.` characters', async () => { cb(null, Buffer.from(JSON.stringify({ id: 'some.name', version: 'some-version' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Plugin "id" must not include \`.\` characters. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); +test('logs warning if pluginId is not in camelCase format', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'some_name', version: 'kibana', server: true }))); + }); + + expect(loggingServiceMock.collect(logger).warn).toHaveLength(0); + await parseManifest(pluginPath, packageInfo, logger); + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Expect plugin \\"id\\" in camelCase, but found: some_name", + ], + ] + `); +}); + test('return error when plugin version is missing', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin manifest for "some-id" must contain a "version" property. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin manifest for "someId" must contain a "version" property. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -112,11 +130,11 @@ test('return error when plugin version is missing', async () => { test('return error when plugin expected Kibana version is lower than actual version', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '6.4.2' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '6.4.2' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin "some-id" is only compatible with Kibana version "6.4.2", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin "someId" is only compatible with Kibana version "6.4.2", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.IncompatibleVersion, path: pluginManifestPath, }); @@ -126,12 +144,12 @@ test('return error when plugin expected Kibana version cannot be interpreted as mockReadFile.mockImplementation((path, cb) => { cb( null, - Buffer.from(JSON.stringify({ id: 'some-id', version: '1.0.0', kibanaVersion: 'non-sem-ver' })) + Buffer.from(JSON.stringify({ id: 'someId', version: '1.0.0', kibanaVersion: 'non-sem-ver' })) ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin "some-id" is only compatible with Kibana version "non-sem-ver", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin "someId" is only compatible with Kibana version "non-sem-ver", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.IncompatibleVersion, path: pluginManifestPath, }); @@ -139,11 +157,11 @@ test('return error when plugin expected Kibana version cannot be interpreted as test('return error when plugin config path is not a string', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', configPath: 2 }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', configPath: 2 }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `The "configPath" in plugin manifest for "some-id" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `The "configPath" in plugin manifest for "someId" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -153,12 +171,12 @@ test('return error when plugin config path is an array that contains non-string mockReadFile.mockImplementation((path, cb) => { cb( null, - Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', configPath: ['config', 2] })) + Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', configPath: ['config', 2] })) ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `The "configPath" in plugin manifest for "some-id" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `The "configPath" in plugin manifest for "someId" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -166,11 +184,11 @@ test('return error when plugin config path is an array that contains non-string test('return error when plugin expected Kibana version is higher than actual version', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.1' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.1' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin "some-id" is only compatible with Kibana version "7.0.1", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin "someId" is only compatible with Kibana version "7.0.1", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.IncompatibleVersion, path: pluginManifestPath, }); @@ -178,11 +196,11 @@ test('return error when plugin expected Kibana version is higher than actual ver test('return error when both `server` and `ui` are set to `false` or missing', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "some-id", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "someId", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -190,12 +208,12 @@ test('return error when both `server` and `ui` are set to `false` or missing', a mockReadFile.mockImplementation((path, cb) => { cb( null, - Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', server: false, ui: false })) + Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', server: false, ui: false })) ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "some-id", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "someId", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -207,7 +225,7 @@ test('return error when manifest contains unrecognized properties', async () => null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', version: '7.0.0', server: true, unknownOne: 'one', @@ -217,21 +235,69 @@ test('return error when manifest contains unrecognized properties', async () => ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Manifest for plugin "some-id" contains the following unrecognized properties: unknownOne,unknownTwo. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Manifest for plugin "someId" contains the following unrecognized properties: unknownOne,unknownTwo. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); +describe('configPath', () => { + test('falls back to plugin id if not specified', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'plugin', version: '7.0.0', server: true }))); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toBe(manifest.id); + }); + + test('falls back to plugin id in snakeCase format', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'SomeId', version: '7.0.0', server: true }))); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toBe('some_id'); + }); + + test('not formated to snakeCase if defined explicitly as string', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb( + null, + Buffer.from( + JSON.stringify({ id: 'someId', configPath: 'somePath', version: '7.0.0', server: true }) + ) + ); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toBe('somePath'); + }); + + test('not formated to snakeCase if defined explicitly as an array of strings', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb( + null, + Buffer.from( + JSON.stringify({ id: 'someId', configPath: ['somePath'], version: '7.0.0', server: true }) + ) + ); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toEqual(['somePath']); + }); +}); + test('set defaults for all missing optional fields', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', server: true }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', server: true }))); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', - configPath: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', + configPath: 'some_id', version: '7.0.0', kibanaVersion: '7.0.0', optionalPlugins: [], @@ -247,7 +313,7 @@ test('return all set optional fields as they are in manifest', async () => { null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', configPath: ['some', 'path'], version: 'some-version', kibanaVersion: '7.0.0', @@ -259,8 +325,8 @@ test('return all set optional fields as they are in manifest', async () => { ); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', configPath: ['some', 'path'], version: 'some-version', kibanaVersion: '7.0.0', @@ -277,7 +343,7 @@ test('return manifest when plugin expected Kibana version matches actual version null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', configPath: 'some-path', version: 'some-version', kibanaVersion: '7.0.0-alpha2', @@ -288,8 +354,8 @@ test('return manifest when plugin expected Kibana version matches actual version ); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', configPath: 'some-path', version: 'some-version', kibanaVersion: '7.0.0-alpha2', @@ -306,7 +372,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async () null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', version: 'some-version', kibanaVersion: 'kibana', requiredPlugins: ['some-required-plugin'], @@ -317,9 +383,9 @@ test('return manifest when plugin expected Kibana version is `kibana`', async () ); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', - configPath: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', + configPath: 'some_id', version: 'some-version', kibanaVersion: 'kibana', optionalPlugins: [], diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index 93c993a0fa3738..573109c9db35a0 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -21,9 +21,12 @@ import { readFile, stat } from 'fs'; import { resolve } from 'path'; import { coerce } from 'semver'; import { promisify } from 'util'; +import { snakeCase } from 'lodash'; import { isConfigPath, PackageInfo } from '../../config'; +import { Logger } from '../../logging'; import { PluginManifest } from '../types'; import { PluginDiscoveryError } from './plugin_discovery_error'; +import { isCamelCase } from './is_camel_case'; const fsReadFileAsync = promisify(readFile); const fsStatAsync = promisify(stat); @@ -67,7 +70,7 @@ const KNOWN_MANIFEST_FIELDS = (() => { * @param packageInfo Kibana package info. * @internal */ -export async function parseManifest(pluginPath: string, packageInfo: PackageInfo) { +export async function parseManifest(pluginPath: string, packageInfo: PackageInfo, log: Logger) { const manifestPath = resolve(pluginPath, MANIFEST_FILE_NAME); let manifestContent; @@ -107,6 +110,10 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo ); } + if (!isCamelCase(manifest.id)) { + log.warn(`Expect plugin "id" in camelCase, but found: ${manifest.id}`); + } + if (!manifest.version || typeof manifest.version !== 'string') { throw PluginDiscoveryError.invalidManifest( manifestPath, @@ -161,7 +168,7 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo id: manifest.id, version: manifest.version, kibanaVersion: expectedKibanaVersion, - configPath: manifest.configPath || manifest.id, + configPath: manifest.configPath || snakeCase(manifest.id), requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [], optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [], ui: includesUiPlugin, diff --git a/src/core/server/plugins/discovery/plugins_discovery.ts b/src/core/server/plugins/discovery/plugins_discovery.ts index 79238afdf5c81b..e7f82c9dc15ada 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.ts @@ -112,7 +112,7 @@ function processPluginSearchPaths$(pluginDirs: readonly string[], log: Logger) { * @param coreContext Kibana core context. */ function createPlugin$(path: string, log: Logger, coreContext: CoreContext) { - return from(parseManifest(path, coreContext.env.packageInfo)).pipe( + return from(parseManifest(path, coreContext.env.packageInfo, log)).pipe( map(manifest => { log.debug(`Successfully discovered plugin "${manifest.id}" at "${path}"`); const opaqueId = Symbol(manifest.id); diff --git a/src/legacy/server/url_shortening/routes/create_routes.js b/src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts similarity index 70% rename from src/legacy/server/url_shortening/routes/create_routes.js rename to src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts index 9540e7441a268b..d81a7eb5db4ae6 100644 --- a/src/legacy/server/url_shortening/routes/create_routes.js +++ b/src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts @@ -17,11 +17,11 @@ * under the License. */ -import { shortUrlLookupProvider } from './lib/short_url_lookup'; -import { createGotoRoute } from './goto'; +export const mockPackage = new Proxy( + { raw: { __dirname: '/tmp' } as any }, + { get: (obj, prop) => obj.raw[prop] } +); +jest.mock('../../../../core/server/utils/package_json', () => ({ pkg: mockPackage })); -export function createRoutes(server) { - const shortUrlLookup = shortUrlLookupProvider(server); - - server.route(createGotoRoute({ server, shortUrlLookup })); -} +export const mockDiscover = jest.fn(); +jest.mock('../discovery/plugins_discovery', () => ({ discover: mockDiscover })); diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts new file mode 100644 index 00000000000000..d5531478f03c54 --- /dev/null +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -0,0 +1,167 @@ +/* + * 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 { mockPackage, mockDiscover } from './plugins_service.test.mocks'; + +import { join } from 'path'; + +import { PluginsService } from '../plugins_service'; +import { ConfigPath, ConfigService, Env } from '../../config'; +import { getEnvOptions } from '../../config/__mocks__/env'; +import { BehaviorSubject, from } from 'rxjs'; +import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; +import { config } from '../plugins_config'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { coreMock } from '../../mocks'; +import { Plugin } from '../types'; +import { PluginWrapper } from '../plugin'; + +describe('PluginsService', () => { + const logger = loggingServiceMock.create(); + let pluginsService: PluginsService; + + const createPlugin = ( + id: string, + { + path = id, + disabled = false, + version = 'some-version', + requiredPlugins = [], + optionalPlugins = [], + kibanaVersion = '7.0.0', + configPath = [path], + server = true, + ui = true, + }: { + path?: string; + disabled?: boolean; + version?: string; + requiredPlugins?: string[]; + optionalPlugins?: string[]; + kibanaVersion?: string; + configPath?: ConfigPath; + server?: boolean; + ui?: boolean; + } + ): PluginWrapper => { + return new PluginWrapper({ + path, + manifest: { + id, + version, + configPath: `${configPath}${disabled ? '-disabled' : ''}`, + kibanaVersion, + requiredPlugins, + optionalPlugins, + server, + ui, + }, + opaqueId: Symbol(id), + initializerContext: { logger } as any, + }); + }; + + beforeEach(async () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; + + const env = Env.createDefault(getEnvOptions()); + const config$ = new BehaviorSubject>({ + plugins: { + initialize: true, + }, + }); + const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); + const configService = new ConfigService(rawConfigService, env, logger); + await configService.setSchema(config.path, config.schema); + + pluginsService = new PluginsService({ + coreId: Symbol('core'), + env, + logger, + configService, + }); + }); + + it("properly resolves `getStartServices` in plugin's lifecycle", async () => { + expect.assertions(5); + + const pluginPath = 'plugin-path'; + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-id', { + path: pluginPath, + configPath: 'path', + }), + ]), + }); + + let startDependenciesResolved = false; + let contextFromStart: any = null; + let contextFromStartService: any = null; + + const pluginInitializer = () => + ({ + setup: async (coreSetup, deps) => { + coreSetup.getStartServices().then(([core, plugins]) => { + startDependenciesResolved = true; + contextFromStartService = { core, plugins }; + }); + }, + start: async (core, plugins) => { + contextFromStart = { core, plugins }; + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + }, + } as Plugin); + + jest.doMock( + join(pluginPath, 'server'), + () => ({ + plugin: pluginInitializer, + }), + { + virtual: true, + } + ); + + await pluginsService.discover(); + + const setupDeps = coreMock.createInternalSetup(); + await pluginsService.setup(setupDeps); + + expect(startDependenciesResolved).toBe(false); + + const startDeps = coreMock.createInternalStart(); + await pluginsService.start(startDeps); + + expect(startDependenciesResolved).toBe(true); + expect(contextFromStart!.core).toEqual(contextFromStartService!.core); + expect(contextFromStart!.plugins).toEqual(contextFromStartService!.plugins); + }); +}); diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 10259b718577c9..6875302f88a9da 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -237,6 +237,43 @@ test('`start` calls plugin.start with context and dependencies', async () => { expect(mockPluginInstance.start).toHaveBeenCalledWith(context, deps); }); +test("`start` resolves `startDependencies` Promise after plugin's start", async () => { + expect.assertions(2); + + const manifest = createPluginManifest(); + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', + manifest, + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); + const startContext = { any: 'thing' } as any; + const pluginDeps = { someDep: 'value' }; + + let startDependenciesResolved = false; + + const mockPluginInstance = { + setup: jest.fn(), + start: async () => { + // delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + }, + }; + mockPluginInitializer.mockReturnValue(mockPluginInstance); + + await plugin.setup({} as any, {} as any); + + const startDependenciesCheck = plugin.startDependencies.then(resolvedStartDeps => { + startDependenciesResolved = true; + expect(resolvedStartDeps).toEqual([startContext, pluginDeps]); + }); + + await plugin.start(startContext, pluginDeps); + await startDependenciesCheck; +}); + test('`stop` fails if plugin is not set up', async () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index c0b484515ccce2..d6c774f6fc41c7 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -19,7 +19,8 @@ import { join } from 'path'; import typeDetect from 'type-detect'; - +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { Type } from '@kbn/config-schema'; import { Logger } from '../logging'; @@ -60,6 +61,9 @@ export class PluginWrapper< private instance?: Plugin; + private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart]>(); + public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); + constructor( public readonly params: { readonly path: string; @@ -88,12 +92,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = this.createPluginInstance(); this.log.info('Setting up plugin'); - return await this.instance.setup(setupContext, plugins); + return this.instance.setup(setupContext, plugins); } /** @@ -108,7 +112,9 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - return await this.instance.start(startContext, plugins); + const startContract = await this.instance.start(startContext, plugins); + this.startDependencies$.next([startContext, plugins]); + return startContract; } /** diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 6d82a8d3ec6cf1..f266172cb4bd9e 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -176,6 +176,7 @@ export function createPluginSetupContext( uuid: { getInstanceUuid: deps.uuid.getInstanceUuid, }, + getStartServices: () => plugin.startDependencies, }; } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index e717871912f46f..a89e2f8c684e40 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -105,7 +105,8 @@ export type PluginOpaqueId = symbol; */ export interface PluginManifest { /** - * Identifier of the plugin. + * Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. + * Other plugins leverage it to access plugin API, navigate to the plugin, etc. */ readonly id: PluginName; @@ -121,7 +122,11 @@ export interface PluginManifest { /** * Root {@link ConfigPath | configuration path} used by the plugin, defaults - * to "id". + * to "id" in snake_case format. + * + * @example + * id: myPlugin + * configPath: my_plugin */ readonly configPath: ConfigPath; @@ -162,7 +167,7 @@ export interface DiscoveredPlugin { readonly id: PluginName; /** - * Root configuration path used by the plugin, defaults to "id". + * Root configuration path used by the plugin, defaults to "id" in snake_case format. */ readonly configPath: ConfigPath; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6a58666716f420..6e41a4aefba302 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -552,13 +552,14 @@ export interface ContextSetup { export type CoreId = symbol; // @public -export interface CoreSetup { +export interface CoreSetup { // (undocumented) capabilities: CapabilitiesSetup; // (undocumented) context: ContextSetup; // (undocumented) elasticsearch: ElasticsearchServiceSetup; + getStartServices(): Promise<[CoreStart, TPluginsStart]>; // (undocumented) http: HttpServiceSetup; // (undocumented) @@ -2005,9 +2006,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:161:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:162:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:222:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:223:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:227:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:228:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts b/src/core/types/app_category.ts similarity index 50% rename from src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts rename to src/core/types/app_category.ts index d9dea35a8a1c03..83a3693f009b6c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_app_state_mock.ts +++ b/src/core/types/app_category.ts @@ -17,32 +17,36 @@ * under the License. */ -import { AppStateClass } from '../legacy_imports'; +/** @public */ /** - * A poor excuse for a mock just to get some basic tests to run in jest without requiring the injector. - * This could be improved if we extract the appState and state classes externally of their angular providers. - * @return {AppStateMock} + * A category definition for nav links to know where to sort them in the left hand nav + * @public */ -export function getAppStateMock(): AppStateClass { - class AppStateMock { - constructor(defaults: any) { - Object.assign(this, defaults); - } +export interface AppCategory { + /** + * Label used for cateogry name. + * Also used as aria-label if one isn't set. + */ + label: string; - on() {} - off() {} - toJSON() { - return ''; - } - save() {} - translateHashToRison(stateHashOrRison: string | string[]) { - return stateHashOrRison; - } - getQueryParamName() { - return ''; - } - } + /** + * If the visual label isn't appropriate for screen readers, + * can override it here + */ + ariaLabel?: string; - return AppStateMock; + /** + * The order that categories will be sorted in + * Prefer large steps between categories to allow for further editing + * (Default categories are in steps of 1000) + */ + order?: number; + + /** + * Define an icon to be used for the category + * If the category is only 1 item, and no icon is defined, will default to the product icon + * Defaults to initials if no icon is defined + */ + euiIconType?: string; } diff --git a/src/core/types/index.ts b/src/core/types/index.ts index d01b514c770a77..7ddb6b0d8dfbbc 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -23,3 +23,4 @@ */ export * from './core_service'; export * from './capabilities'; +export * from './app_category'; diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts new file mode 100644 index 00000000000000..3e3cc2fef2a229 --- /dev/null +++ b/src/core/utils/default_app_categories.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +/** @internal */ +export const DEFAULT_APP_CATEGORIES = Object.freeze({ + analyze: { + label: i18n.translate('core.ui.analyzeNavList.label', { + defaultMessage: 'Analyze', + }), + order: 1000, + }, + observability: { + label: i18n.translate('core.ui.observabilityNavList.label', { + defaultMessage: 'Observability', + }), + order: 2000, + }, + security: { + label: i18n.translate('core.ui.securityNavList.label', { + defaultMessage: 'Security', + }), + order: 3000, + }, + management: { + label: i18n.translate('core.ui.managementNavList.label', { + defaultMessage: 'Management', + }), + euiIconType: 'managementApp', + }, +}); diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 7c8ed481c0a7d0..7317c222d3bc32 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -28,3 +28,4 @@ export * from './pick'; export * from './promise'; export * from './url'; export * from './unset'; +export * from './default_app_categories'; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 1c95e75396bcc2..807a3fbf4782bf 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -58,6 +58,7 @@ export default { '^ui/(.*)': '/src/legacy/ui/public/$1', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', + '^fixtures/(.*)': '/src/fixtures/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap index 0a40e3e84211dc..36f4dec1a1f543 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap @@ -8,13 +8,13 @@ exports[`Split panel should render correctly 1`] = ` "panels": Array [ Object { "getWidth": [Function], - "initialWidth": "100%", "setWidth": [Function], + "width": 100, }, Object { "getWidth": [Function], - "initialWidth": "100%", "setWidth": [Function], + "width": 100, }, ], } @@ -55,15 +55,39 @@ exports[`Split panel should render correctly 1`] = ` -
- ︙ -
+ + + + + +
; +export type ResizerMouseEvent = React.MouseEvent; +export type ResizerKeyDownEvent = React.KeyboardEvent; export interface Props { + onKeyDown: (eve: ResizerKeyDownEvent) => void; onMouseDown: (eve: ResizerMouseEvent) => void; + className?: string; } -/** - * TODO: This component uses styling constants from public UI - should be removed, next iteration should incl. horizontal and vertical resizers. - */ export function Resizer(props: Props) { return ( -
- ︙ -
+ ); } diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx index 80960a7772ba1b..2eb39f0808ad02 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx @@ -22,20 +22,26 @@ import { usePanelContext } from '../context'; export interface Props { children: ReactNode[] | ReactNode; - initialWidth?: string; + className?: string; + + /** + * initial width of the panel in percents + */ + initialWidth?: number; style?: CSSProperties; } -export function Panel({ children, initialWidth = '100%', style = {} }: Props) { - const [width, setWidth] = useState(initialWidth); +export function Panel({ children, className, initialWidth = 100, style = {} }: Props) { + const [width, setWidth] = useState(`${initialWidth}%`); const { registry } = usePanelContext(); const divRef = useRef(null); useEffect(() => { registry.registerPanel({ - initialWidth, + width: initialWidth, setWidth(value) { setWidth(value + '%'); + this.width = value; }, getWidth() { return divRef.current!.getBoundingClientRect().width; @@ -44,7 +50,7 @@ export function Panel({ children, initialWidth = '100%', style = {} }: Props) { }, [initialWidth, registry]); return ( -
+
{children}
); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx index fef65a954bd600..c9d7b01f87967b 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx @@ -17,14 +17,17 @@ * under the License. */ -import React, { Children, ReactNode, useRef, useState } from 'react'; +import React, { Children, ReactNode, useRef, useState, useCallback } from 'react'; +import { keyCodes } from '@elastic/eui'; import { PanelContextProvider } from '../context'; -import { Resizer } from '../components/resizer'; +import { Resizer, ResizerMouseEvent, ResizerKeyDownEvent } from '../components/resizer'; import { PanelRegistry } from '../registry'; export interface Props { children: ReactNode; + className?: string; + resizerClassName?: string; onPanelWidthChange?: (arrayOfPanelWidths: number[]) => any; } @@ -37,7 +40,12 @@ const initialState: State = { isDragging: false, currentResizerPos: -1 }; const pxToPercent = (proportion: number, whole: number) => (proportion / whole) * 100; -export function PanelsContainer({ children, onPanelWidthChange }: Props) { +export function PanelsContainer({ + children, + className, + onPanelWidthChange, + resizerClassName, +}: Props) { const [firstChild, secondChild] = Children.toArray(children); const registryRef = useRef(new PanelRegistry()); @@ -48,18 +56,48 @@ export function PanelsContainer({ children, onPanelWidthChange }: Props) { return containerRef.current!.getBoundingClientRect().width; }; + const handleMouseDown = useCallback( + (event: ResizerMouseEvent) => { + setState({ + ...state, + isDragging: true, + currentResizerPos: event.clientX, + }); + }, + [state] + ); + + const handleKeyDown = useCallback( + (ev: ResizerKeyDownEvent) => { + const { keyCode } = ev; + + if (keyCode === keyCodes.LEFT || keyCode === keyCodes.RIGHT) { + ev.preventDefault(); + + const { current: registry } = registryRef; + const [left, right] = registry.getPanels(); + + const leftPercent = left.width - (keyCode === keyCodes.LEFT ? 1 : -1); + const rightPercent = right.width - (keyCode === keyCodes.RIGHT ? 1 : -1); + + left.setWidth(leftPercent); + right.setWidth(rightPercent); + + if (onPanelWidthChange) { + onPanelWidthChange([leftPercent, rightPercent]); + } + } + }, + [onPanelWidthChange] + ); + const childrenWithResizer = [ firstChild, { - event.preventDefault(); - setState({ - ...state, - isDragging: true, - currentResizerPos: event.clientX, - }); - }} + className={resizerClassName} + onKeyDown={handleKeyDown} + onMouseDown={handleMouseDown} />, secondChild, ]; @@ -67,6 +105,7 @@ export function PanelsContainer({ children, onPanelWidthChange }: Props) { return (
{ diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts index 5f06ab8915270f..e275da9e2ac744 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts @@ -20,7 +20,7 @@ export interface PanelController { setWidth: (percent: number) => void; getWidth: () => number; - initialWidth: string; + width: number; } export class PanelRegistry { @@ -35,6 +35,6 @@ export class PanelRegistry { } getPanels() { - return this.panels.map(panel => ({ ...panel })); + return this.panels; } } diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx index 304535421a78a6..02153d1a1d3cd4 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx @@ -65,8 +65,8 @@ describe('Split panel', () => { const panelContainer = mount( - {testComponentA} - {testComponentB} + {testComponentA} + {testComponentB} ); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx index 56449bfb454174..7be1382760eb91 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx @@ -55,10 +55,10 @@ export const Editor = ({ loading }: Props) => { if (!currentTextObject) return null; return ( - + {loading ? ( @@ -68,7 +68,7 @@ export const Editor = ({ loading }: Props) => { {loading ? : } diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/input_highlight_rules.js b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/input_highlight_rules.js index b7b6c5bade9ad7..842736428e8bba 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/input_highlight_rules.js +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/input_highlight_rules.js @@ -89,6 +89,7 @@ export function InputHighlightRules() { addEOL(['url.amp'], /(&)/, 'start') ), 'url-sql': mergeTokens( + addEOL(['url.part'], /([^?\/,\s]+)/, 'start-sql'), addEOL(['url.comma'], /(,)/, 'start-sql'), addEOL(['url.slash'], /(\/)/, 'start-sql'), addEOL(['url.questionmark'], /(\?)/, 'start-sql', 'urlParams-sql') diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts index 61821b7ad45e94..ad5c91d2e19de1 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts @@ -18,10 +18,18 @@ */ import _ from 'lodash'; +import { Subscription } from 'rxjs'; import { State } from 'ui/state_management/state'; import { FilterManager, esFilters } from '../../../../../../plugins/data/public'; -type GetAppStateFunc = () => State | undefined | null; +import { + compareFilters, + COMPARE_ALL_OPTIONS, + // this whole file will soon be deprecated by new state management. + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/query/filter_manager/lib/compare_filters'; + +type GetAppStateFunc = () => { filters?: esFilters.Filter[]; save?: () => void } | undefined | null; /** * FilterStateManager is responsible for watching for filter changes @@ -29,10 +37,12 @@ type GetAppStateFunc = () => State | undefined | null; * back to the URL. **/ export class FilterStateManager { + private filterManagerUpdatesSubscription: Subscription; + filterManager: FilterManager; globalState: State; getAppState: GetAppStateFunc; - interval: NodeJS.Timeout | undefined; + interval: number | undefined; constructor(globalState: State, getAppState: GetAppStateFunc, filterManager: FilterManager) { this.getAppState = getAppState; @@ -41,7 +51,7 @@ export class FilterStateManager { this.watchFilterState(); - this.filterManager.getUpdates$().subscribe(() => { + this.filterManagerUpdatesSubscription = this.filterManager.getUpdates$().subscribe(() => { this.updateAppState(); }); } @@ -50,12 +60,13 @@ export class FilterStateManager { if (this.interval) { clearInterval(this.interval); } + this.filterManagerUpdatesSubscription.unsubscribe(); } private watchFilterState() { // This is a temporary solution to remove rootscope. // Moving forward, state should provide observable subscriptions. - this.interval = setInterval(() => { + this.interval = window.setInterval(() => { const appState = this.getAppState(); const stateUndefined = !appState || !this.globalState; if (stateUndefined) return; @@ -63,8 +74,16 @@ export class FilterStateManager { const globalFilters = this.globalState.filters || []; const appFilters = (appState && appState.filters) || []; - const globalFilterChanged = !_.isEqual(this.filterManager.getGlobalFilters(), globalFilters); - const appFilterChanged = !_.isEqual(this.filterManager.getAppFilters(), appFilters); + const globalFilterChanged = !compareFilters( + this.filterManager.getGlobalFilters(), + globalFilters, + COMPARE_ALL_OPTIONS + ); + const appFilterChanged = !compareFilters( + this.filterManager.getAppFilters(), + appFilters, + COMPARE_ALL_OPTIONS + ); const filterStateChanged = globalFilterChanged || appFilterChanged; if (!filterStateChanged) return; @@ -80,7 +99,7 @@ export class FilterStateManager { private saveState() { const appState = this.getAppState(); - if (appState) appState.save(); + if (appState && appState.save) appState.save(); this.globalState.save(); } diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 893e477b38583d..5329702348207f 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -20,15 +20,24 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { SearchService, SearchStart } from './search'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { setFieldFormats, setNotifications, setIndexPatterns, setQueryService, + setSearchService, + setUiSettings, + setInjectedMetadata, + setHttp, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +export interface DataPluginSetupDependencies { + expressions: ExpressionsSetup; +} + export interface DataPluginStartDependencies { data: DataPublicPluginStart; } @@ -54,18 +63,24 @@ export interface DataStart { * or static code. */ -export class DataPlugin implements Plugin { +export class DataPlugin + implements Plugin { private readonly search = new SearchService(); - public setup(core: CoreSetup) {} + public setup(core: CoreSetup) { + setInjectedMetadata(core.injectedMetadata); + } public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { // This is required for when Angular code uses Field and FieldList. setFieldFormats(data.fieldFormats); setQueryService(data.query); + setSearchService(data.search); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setNotifications(core.notifications); + setUiSettings(core.uiSettings); + setHttp(core.http); return { search: this.search.start(core), diff --git a/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts similarity index 95% rename from src/legacy/ui/public/inspector/build_tabular_inspector_data.ts rename to src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index b09ed60e7186f9..6e6d2a15fa2ac8 100644 --- a/src/legacy/ui/public/inspector/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,8 +19,8 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../core_plugins/visualizations/public'; -import { FormattedData } from './adapters'; +import { createFilter } from '../../../../visualizations/public'; +import { FormattedData } from '../../../../../../plugins/inspector/public'; interface Column { id: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 4ec4dbd7f88d69..889c747c9a62e3 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -34,14 +34,8 @@ import { getTime, FilterManager, } from '../../../../../../plugins/data/public'; -import { - SearchSource, - ISearchSource, - getRequestInspectorStats, - getResponseInspectorStats, -} from '../../../../../ui/public/courier'; -import { buildTabularInspectorData } from '../../../../../ui/public/inspector/build_tabular_inspector_data'; +import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; // @ts-ignore import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; @@ -49,6 +43,8 @@ import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; +import { ISearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { SearchSource } from '../search_source'; export interface RequestHandlerParams { searchSource: ISearchSource; diff --git a/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts b/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts index 74c87d77dd4fd0..24a36c9db9df76 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/call_client.test.ts @@ -76,7 +76,7 @@ describe('callClient', () => { test('Passes the additional arguments it is given to the search strategy', () => { const searchRequests = [{ _searchStrategyId: 0 }]; - const args = { es: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; + const args = { searchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); diff --git a/src/legacy/core_plugins/data/public/search/fetch/call_client.ts b/src/legacy/core_plugins/data/public/search/fetch/call_client.ts index 43da27f941e4e2..ad18775d5f1448 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/call_client.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/call_client.ts @@ -26,7 +26,7 @@ import { SearchRequest } from '../types'; export function callClient( searchRequests: SearchRequest[], requestsOptions: FetchOptions[] = [], - { es, config, esShardTimeout }: FetchHandlers + fetchHandlers: FetchHandlers ) { // Correlate the options with the request that they're associated with const requestOptionEntries: Array<[ @@ -53,9 +53,7 @@ export function callClient( // then an error would have been thrown above const { searching, abort } = searchStrategy!.search({ searchRequests: requests, - es, - config, - esShardTimeout, + ...fetchHandlers, }); requests.forEach((request, i) => { diff --git a/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts b/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts index 75de85e02a1a2b..4830464047ad66 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/fetch_soon.ts @@ -28,10 +28,10 @@ import { SearchRequest, SearchResponse } from '../types'; export async function fetchSoon( request: SearchRequest, options: FetchOptions, - { es, config, esShardTimeout }: FetchHandlers + fetchHandlers: FetchHandlers ) { - const msToDelay = config.get('courier:batchSearches') ? 50 : 0; - return delayedFetch(request, options, { es, config, esShardTimeout }, msToDelay); + const msToDelay = fetchHandlers.config.get('courier:batchSearches') ? 50 : 0; + return delayedFetch(request, options, fetchHandlers, msToDelay); } /** @@ -64,7 +64,7 @@ let fetchInProgress: Promise | null = null; async function delayedFetch( request: SearchRequest, options: FetchOptions, - { es, config, esShardTimeout }: FetchHandlers, + fetchHandlers: FetchHandlers, ms: number ) { const i = requestsToFetch.length; @@ -73,7 +73,7 @@ async function delayedFetch( const responses = await (fetchInProgress = fetchInProgress || delay(() => { - const response = callClient(requestsToFetch, requestOptions, { es, config, esShardTimeout }); + const response = callClient(requestsToFetch, requestOptions, fetchHandlers); requestsToFetch = []; requestOptions = []; fetchInProgress = null; diff --git a/src/legacy/core_plugins/data/public/search/fetch/types.ts b/src/legacy/core_plugins/data/public/search/fetch/types.ts index 0887a1f84c7c8c..fba14119d83c37 100644 --- a/src/legacy/core_plugins/data/public/search/fetch/types.ts +++ b/src/legacy/core_plugins/data/public/search/fetch/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ISearchStart } from 'src/plugins/data/public'; import { IUiSettingsClient } from '../../../../../../core/public'; import { SearchRequest, SearchResponse } from '../types'; @@ -35,7 +36,7 @@ export interface FetchOptions { } export interface FetchHandlers { - es: ApiCaller; + searchService: ISearchStart; config: IUiSettingsClient; esShardTimeout: number; } diff --git a/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts b/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts index 28f8dba9a75de3..ebeee60b67c8ad 100644 --- a/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts +++ b/src/legacy/core_plugins/data/public/search/search_source/search_source.test.ts @@ -19,19 +19,34 @@ import { SearchSource } from '../search_source'; import { IndexPattern } from '../../../../../../plugins/data/public'; - -jest.mock('ui/new_platform'); +import { + setSearchService, + setUiSettings, + setInjectedMetadata, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/services'; + +import { + injectedMetadataServiceMock, + uiSettingsServiceMock, +} from '../../../../../../core/public/mocks'; + +setUiSettings(uiSettingsServiceMock.createStartContract()); +setInjectedMetadata(injectedMetadataServiceMock.createSetupContract()); +setSearchService({ + search: jest.fn(), + __LEGACY: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, +}); jest.mock('../fetch', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), })); -jest.mock('ui/chrome', () => ({ - dangerouslyGetActiveInjector: () => ({ - get: jest.fn(), - }), -})); - const getComputedFields = () => ({ storedFields: [], scriptFields: [], diff --git a/src/legacy/core_plugins/data/public/search/search_source/search_source.ts b/src/legacy/core_plugins/data/public/search/search_source/search_source.ts index 6efcae4d4b88dc..e977db713ebaa8 100644 --- a/src/legacy/core_plugins/data/public/search/search_source/search_source.ts +++ b/src/legacy/core_plugins/data/public/search/search_source/search_source.ts @@ -70,8 +70,6 @@ */ import _ from 'lodash'; -import { npSetup } from 'ui/new_platform'; -import chrome from 'ui/chrome'; import { normalizeSortRequest } from './normalize_sort_request'; import { fetchSoon } from '../fetch'; import { fieldWildcardFilter } from '../../../../../../plugins/kibana_utils/public'; @@ -79,10 +77,14 @@ import { getHighlightRequest, esFilters, esQuery } from '../../../../../../plugi import { RequestFailure } from '../fetch/errors'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { SearchSourceOptions, SearchSourceFields, SearchRequest } from './types'; -import { FetchOptions, ApiCaller } from '../fetch/types'; +import { FetchOptions } from '../fetch/types'; -const esShardTimeout = npSetup.core.injectedMetadata.getInjectedVar('esShardTimeout') as number; -const config = npSetup.core.uiSettings; +import { + getSearchService, + getUiSettings, + getInjectedMetadata, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/data/public/services'; export type ISearchSource = Pick; @@ -192,21 +194,23 @@ export class SearchSource { * @async */ async fetch(options: FetchOptions = {}) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const es = $injector.get('es') as ApiCaller; - await this.requestIsStarting(options); const searchRequest = await this.flatten(); this.history = [searchRequest]; + const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; const response = await fetchSoon( searchRequest, { ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), ...options, }, - { es, config, esShardTimeout } + { + searchService: getSearchService(), + config: getUiSettings(), + esShardTimeout, + } ); if (response.error) { @@ -313,7 +317,11 @@ export class SearchSource { case 'source': return addToBody('_source', val); case 'sort': - const sort = normalizeSortRequest(val, this.getField('index'), config.get('sort:options')); + const sort = normalizeSortRequest( + val, + this.getField('index'), + getUiSettings().get('sort:options') + ); return addToBody(key, sort); default: return addToBody(key, val); @@ -359,7 +367,7 @@ export class SearchSource { if (body._source) { // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, config.get('metaFields')); + const filter = fieldWildcardFilter(body._source.excludes, getUiSettings().get('metaFields')); body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => filter(docvalueField.field) ); @@ -377,11 +385,11 @@ export class SearchSource { _.set(body, '_source.includes', remainingFields); } - const esQueryConfigs = esQuery.getEsQueryConfig(config); + const esQueryConfigs = esQuery.getEsQueryConfig(getUiSettings()); body.query = esQuery.buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, config.get('doc_table:highlight')); + body.highlight = getHighlightRequest(body.query, getUiSettings().get('doc_table:highlight')); delete searchRequest.highlightAll; } diff --git a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts index 0ec6a6c2e143e7..8caf20c50cd3aa 100644 --- a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts +++ b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.test.ts @@ -37,9 +37,16 @@ const searchMockResponse: any = Promise.resolve([]); searchMockResponse.abort = jest.fn(); const searchMock = jest.fn().mockReturnValue(searchMockResponse); +const newSearchMockResponse: any = Promise.resolve([]); +newSearchMockResponse.abort = jest.fn(); +const newSearchMock = jest.fn().mockReturnValue({ + toPromise: () => searchMockResponse, +}); + describe('defaultSearchStrategy', function() { describe('search', function() { let searchArgs: MockedKeys>; + let es: any; beforeEach(() => { msearchMockResponse.abort.mockClear(); @@ -55,17 +62,24 @@ describe('defaultSearchStrategy', function() { }, ], esShardTimeout: 0, - es: { - msearch: msearchMock, - search: searchMock, + searchService: { + search: newSearchMock, + __LEGACY: { + esClient: { + search: searchMock, + msearch: msearchMock, + }, + }, }, }; + + es = searchArgs.searchService.__LEGACY.esClient; }); test('does not send max_concurrent_shard_requests by default', async () => { const config = getConfigStub({ 'courier:batchSearches': true }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); + expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); }); test('allows configuration of max_concurrent_shard_requests', async () => { @@ -74,13 +88,13 @@ describe('defaultSearchStrategy', function() { 'courier:maxConcurrentShardRequests': 42, }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); + expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); }); test('should set rest_total_hits_as_int to true on a request', async () => { const config = getConfigStub({ 'courier:batchSearches': true }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); + expect(es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); }); test('should set ignore_throttled=false when including frozen indices', async () => { @@ -89,7 +103,7 @@ describe('defaultSearchStrategy', function() { 'search:includeFrozen': true, }); await search({ ...searchArgs, config }); - expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); + expect(es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); test('should properly call abort with msearch', () => { @@ -100,12 +114,18 @@ describe('defaultSearchStrategy', function() { expect(msearchMockResponse.abort).toHaveBeenCalled(); }); - test('should properly abort with search', async () => { - const config = getConfigStub({ - 'courier:batchSearches': false, - }); + test('should call new search service', () => { + const config = getConfigStub(); + search({ ...searchArgs, config }); + expect(searchMock).toHaveBeenCalled(); + expect(newSearchMock).toHaveBeenCalledTimes(0); + }); + + test('should properly abort with new search service', async () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const config = getConfigStub({}); search({ ...searchArgs, config }).abort(); - expect(searchMockResponse.abort).toHaveBeenCalled(); + expect(abortSpy).toHaveBeenCalled(); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts index 9bfa1df71aa81a..39789504de0a74 100644 --- a/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts +++ b/src/legacy/core_plugins/data/public/search/search_strategy/default_search_strategy.ts @@ -38,7 +38,14 @@ export const defaultSearchStrategy: SearchStrategyProvider = { }, }; -function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { +// @deprecated +function msearch({ + searchRequests, + searchService, + config, + esShardTimeout, +}: SearchStrategySearchParams) { + const es = searchService.__LEGACY.esClient; const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, @@ -57,19 +64,39 @@ function msearch({ searchRequests, es, config, esShardTimeout }: SearchStrategyS ...getMSearchParams(config), body: `${inlineRequests.join('\n')}\n`, }); + return { - searching: searching.then(({ responses }) => responses), + searching: searching.then(({ responses }: any) => responses), abort: searching.abort, }; } -function search({ searchRequests, es, config, esShardTimeout }: SearchStrategySearchParams) { +function search({ + searchRequests, + searchService, + config, + esShardTimeout, +}: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); + const es = searchService.__LEGACY.esClient; const promises = searchRequests.map(({ index, body }) => { const searching = es.search({ index: index.title || index, body, ...searchParams }); abortController.signal.addEventListener('abort', searching.abort); - return searching.catch(({ response }) => JSON.parse(response)); + return searching.catch(({ response }: any) => JSON.parse(response)); + /* + * Once #44302 is resolved, replace the old implementation with this one - + * const params = { + * index: index.title || index, + * body, + * ...searchParams, + * }; + * const { signal } = abortController; + * return searchService + * .search({ params }, { signal }) + * .toPromise() + * .then(({ rawResponse }) => rawResponse); + */ }); return { searching: Promise.all(promises), diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap index 278811ca85df9d..249f42a6ebf3f9 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap @@ -44,11 +44,10 @@ exports[`renders ControlsTab 1`] = ` } } getIndexPattern={[Function]} - handleCheckboxOptionChange={[Function]} handleFieldNameChange={[Function]} handleIndexPatternChange={[Function]} handleLabelChange={[Function]} - handleNumberOptionChange={[Function]} + handleOptionsChange={[Function]} handleParentChange={[Function]} handleRemoveControl={[Function]} key="1" @@ -101,11 +100,10 @@ exports[`renders ControlsTab 1`] = ` } } getIndexPattern={[Function]} - handleCheckboxOptionChange={[Function]} handleFieldNameChange={[Function]} handleIndexPatternChange={[Function]} handleLabelChange={[Function]} - handleNumberOptionChange={[Function]} + handleOptionsChange={[Function]} handleParentChange={[Function]} handleRemoveControl={[Function]} key="2" diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx index dbac5d9d943710..2bd0baea6eff8a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -29,7 +29,6 @@ import { EuiFormRow, EuiPanel, EuiSpacer, - EuiSwitchEvent, } from '@elastic/eui'; import { RangeControlEditor } from './range_control_editor'; @@ -41,33 +40,28 @@ import { InputControlVisDependencies } from '../../plugin'; interface ControlEditorUiProps { controlIndex: number; controlParams: ControlParams; - handleLabelChange: (controlIndex: number, event: ChangeEvent) => void; + handleLabelChange: (controlIndex: number, value: string) => void; moveControl: (controlIndex: number, direction: number) => void; handleRemoveControl: (controlIndex: number) => void; handleIndexPatternChange: (controlIndex: number, indexPatternId: string) => void; handleFieldNameChange: (controlIndex: number, fieldName: string) => void; getIndexPattern: (indexPatternId: string) => Promise; - handleCheckboxOptionChange: ( + handleOptionsChange: ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: EuiSwitchEvent - ) => void; - handleNumberOptionChange: ( - controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent + optionName: T, + value: ControlParamsOptions[T] ) => void; parentCandidates: Array<{ value: string; text: string; }>; - handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + handleParentChange: (controlIndex: number, parent: string) => void; deps: InputControlVisDependencies; } class ControlEditorUi extends PureComponent { changeLabel = (event: ChangeEvent) => { - this.props.handleLabelChange(this.props.controlIndex, event); + this.props.handleLabelChange(this.props.controlIndex, event.target.value); }; removeControl = () => { @@ -101,8 +95,7 @@ class ControlEditorUi extends PureComponent ); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx index 56381ef7d1570d..214cff4ddf9d55 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { PureComponent, ChangeEvent } from 'react'; +import React, { PureComponent } from 'react'; import { InjectedIntlProps } from 'react-intl'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; @@ -28,7 +28,6 @@ import { EuiFormRow, EuiPanel, EuiSelect, - EuiSwitchEvent, } from '@elastic/eui'; import { ControlEditor } from './control_editor'; @@ -73,44 +72,44 @@ class ControlsTabUi extends PureComponent this.props.setValue('controls', value); - handleLabelChange = (controlIndex: number, event: ChangeEvent) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.label = event.target.value; + handleLabelChange = (controlIndex: number, label: string) => { + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + label, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleIndexPatternChange = (controlIndex: number, indexPatternId: string) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.indexPattern = indexPatternId; - updatedControl.fieldName = ''; + handleIndexPatternChange = (controlIndex: number, indexPattern: string) => { + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + indexPattern, + fieldName: '', + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; handleFieldNameChange = (controlIndex: number, fieldName: string) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.fieldName = fieldName; + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + fieldName, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleCheckboxOptionChange = ( + handleOptionsChange = ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: EuiSwitchEvent + optionName: T, + value: ControlParamsOptions[T] ) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - // @ts-ignore - updatedControl.options[optionName] = event.target.checked; - this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); - }; - - handleNumberOptionChange = ( - controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent - ) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - // @ts-ignore - updatedControl.options[optionName] = parseFloat(event.target.value); + const control = this.props.stateParams.controls[controlIndex]; + const updatedControl = { + ...control, + options: { + ...control.options, + [optionName]: value, + }, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; @@ -126,9 +125,11 @@ class ControlsTabUi extends PureComponent) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.parent = event.target.value; + handleParentChange = (controlIndex: number, parent: string) => { + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + parent, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; @@ -151,8 +152,7 @@ class ControlsTabUi extends PureComponent { handleFieldNameChange = sinon.spy(); handleIndexPatternChange = sinon.spy(); - handleCheckboxOptionChange = sinon.spy(); - handleNumberOptionChange = sinon.spy(); + handleOptionsChange = sinon.spy(); }); describe('renders', () => { @@ -82,8 +80,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -107,8 +104,7 @@ describe('renders', () => { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={parentCandidates} /> @@ -143,8 +139,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -178,8 +173,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -213,8 +207,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -227,7 +220,7 @@ describe('renders', () => { }); }); -test('handleCheckboxOptionChange - multiselect', async () => { +test('handleOptionsChange - multiselect', async () => { const component = mountWithIntl( { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -249,25 +241,12 @@ test('handleCheckboxOptionChange - multiselect', async () => { checkbox.simulate('click'); sinon.assert.notCalled(handleFieldNameChange); sinon.assert.notCalled(handleIndexPatternChange); - sinon.assert.notCalled(handleNumberOptionChange); const expectedControlIndex = 0; const expectedOptionName = 'multiselect'; - sinon.assert.calledWith( - handleCheckboxOptionChange, - expectedControlIndex, - expectedOptionName, - sinon.match(event => { - // Synthetic `event.target.checked` does not get altered by EuiSwitch, - // but its aria attribute is correctly updated - if (event.target.getAttribute('aria-checked') === 'true') { - return true; - } - return false; - }, 'unexpected checkbox input event') - ); + sinon.assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName); }); -test('handleNumberOptionChange - size', async () => { +test('handleOptionsChange - size', async () => { const component = mountWithIntl( { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -286,23 +264,12 @@ test('handleNumberOptionChange - size', async () => { await updateComponent(component); const input = findTestSubject(component, 'listControlSizeInput'); - input.simulate('change', { target: { value: 7 } }); - sinon.assert.notCalled(handleCheckboxOptionChange); + input.simulate('change', { target: { valueAsNumber: 7 } }); sinon.assert.notCalled(handleFieldNameChange); sinon.assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'size'; - sinon.assert.calledWith( - handleNumberOptionChange, - expectedControlIndex, - expectedOptionName, - sinon.match(event => { - if (event.target.value === 7) { - return true; - } - return false; - }, 'unexpected input event') - ); + sinon.assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName, 7); }); test('field name change', async () => { @@ -314,8 +281,7 @@ test('field name change', async () => { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx index ed68894d39ae42..9772cb5fc25488 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -17,10 +17,10 @@ * under the License. */ -import React, { PureComponent, ChangeEvent, ComponentType } from 'react'; +import React, { PureComponent, ComponentType } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect, EuiSwitchEvent } from '@elastic/eui'; +import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; @@ -45,17 +45,12 @@ interface ListControlEditorProps { controlParams: ControlParams; handleFieldNameChange: (fieldName: string) => void; handleIndexPatternChange: (indexPatternId: string) => void; - handleCheckboxOptionChange: ( + handleOptionsChange: ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: EuiSwitchEvent + optionName: T, + value: ControlParamsOptions[T] ) => void; - handleNumberOptionChange: ( - controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent - ) => void; - handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + handleParentChange: (controlIndex: number, parent: string) => void; parentCandidates: React.ComponentProps['options']; deps: InputControlVisDependencies; } @@ -177,7 +172,7 @@ export class ListControlEditor extends PureComponent< options={parentCandidatesOptions} value={this.props.controlParams.parent} onChange={event => { - this.props.handleParentChange(this.props.controlIndex, event); + this.props.handleParentChange(this.props.controlIndex, event.target.value); }} /> @@ -204,7 +199,11 @@ export class ListControlEditor extends PureComponent< } checked={this.props.controlParams.options.multiselect ?? true} onChange={event => { - this.props.handleCheckboxOptionChange(this.props.controlIndex, 'multiselect', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'multiselect', + event.target.checked + ); }} data-test-subj="listControlMultiselectInput" /> @@ -237,7 +236,11 @@ export class ListControlEditor extends PureComponent< } checked={this.props.controlParams.options.dynamicOptions ?? false} onChange={event => { - this.props.handleCheckboxOptionChange(this.props.controlIndex, 'dynamicOptions', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'dynamicOptions', + event.target.checked + ); }} disabled={this.state.isStringField ? false : true} data-test-subj="listControlDynamicOptionsSwitch" @@ -268,7 +271,11 @@ export class ListControlEditor extends PureComponent< min={1} value={this.props.controlParams.options.size} onChange={event => { - this.props.handleNumberOptionChange(this.props.controlIndex, 'size', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'size', + event.target.valueAsNumber + ); }} data-test-subj="listControlSizeInput" /> diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx index e7f9b6083890c4..55c4c71ce430b2 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SinonSpy, spy, assert, match } from 'sinon'; +import { SinonSpy, spy, assert } from 'sinon'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; // @ts-ignore @@ -46,12 +46,12 @@ const controlParams: ControlParams = { const deps = getDepsMock(); let handleFieldNameChange: SinonSpy; let handleIndexPatternChange: SinonSpy; -let handleNumberOptionChange: SinonSpy; +let handleOptionsChange: SinonSpy; beforeEach(() => { handleFieldNameChange = spy(); handleIndexPatternChange = spy(); - handleNumberOptionChange = spy(); + handleOptionsChange = spy(); }); test('renders RangeControlEditor', async () => { @@ -63,7 +63,7 @@ test('renders RangeControlEditor', async () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} /> ); @@ -72,7 +72,7 @@ test('renders RangeControlEditor', async () => { expect(component).toMatchSnapshot(); // eslint-disable-line }); -test('handleNumberOptionChange - step', async () => { +test('handleOptionsChange - step', async () => { const component = mountWithIntl( { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} /> ); await updateComponent(component); findTestSubject(component, 'rangeControlSizeInput0').simulate('change', { - target: { value: 0.5 }, + target: { valueAsNumber: 0.5 }, }); assert.notCalled(handleFieldNameChange); assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'step'; - assert.calledWith( - handleNumberOptionChange, - expectedControlIndex, - expectedOptionName, - match(event => { - if (event.target.value === 0.5) { - return true; - } - return false; - }, 'unexpected input event') - ); + assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName, 0.5); }); -test('handleNumberOptionChange - decimalPlaces', async () => { +test('handleOptionsChange - decimalPlaces', async () => { const component = mountWithIntl( { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} /> ); await updateComponent(component); findTestSubject(component, 'rangeControlDecimalPlacesInput0').simulate('change', { - target: { value: 2 }, + target: { valueAsNumber: 2 }, }); assert.notCalled(handleFieldNameChange); assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'decimalPlaces'; - assert.calledWith( - handleNumberOptionChange, - expectedControlIndex, - expectedOptionName, - match(event => { - if (event.target.value === 2) { - return true; - } - return false; - }, 'unexpected input event') - ); + assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName, 2); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 44477eafda6b17..97850879a2d382 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { Component, Fragment, ChangeEvent, ComponentType } from 'react'; +import React, { Component, Fragment, ComponentType } from 'react'; import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,10 +37,10 @@ interface RangeControlEditorProps { getIndexPattern: (indexPatternId: string) => Promise; handleFieldNameChange: (fieldName: string) => void; handleIndexPatternChange: (indexPatternId: string) => void; - handleNumberOptionChange: ( + handleOptionsChange: ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent + optionName: T, + value: ControlParamsOptions[T] ) => void; deps: InputControlVisDependencies; } @@ -109,7 +109,11 @@ export class RangeControlEditor extends Component< { - this.props.handleNumberOptionChange(this.props.controlIndex, 'step', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'step', + event.target.valueAsNumber + ); }} data-test-subj={`rangeControlSizeInput${this.props.controlIndex}`} /> @@ -128,7 +132,11 @@ export class RangeControlEditor extends Component< min={0} value={this.props.controlParams.options.decimalPlaces} onChange={event => { - this.props.handleNumberOptionChange(this.props.controlIndex, 'decimalPlaces', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'decimalPlaces', + event.target.valueAsNumber + ); }} data-test-subj={`rangeControlDecimalPlacesInput${this.props.controlIndex}`} /> diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts index b6774aa87b43c2..9473ea5a20b356 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -50,7 +50,6 @@ export function createInputControlVisTypeDefinition(deps: InputControlVisDepende pinFilters: false, }, }, - editor: 'default', editorConfig: { optionTabs: [ { diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index e6a0420534ef25..55bd8520502187 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -34,6 +34,7 @@ import { getUiSettingDefaults } from './ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; const mkdirAsync = promisify(Fs.mkdir); @@ -60,7 +61,12 @@ export default function(kibana) { }, uiExports: { - hacks: ['plugins/kibana/discover', 'plugins/kibana/dev_tools', 'plugins/kibana/visualize'], + hacks: [ + 'plugins/kibana/discover/legacy', + 'plugins/kibana/dev_tools', + 'plugins/kibana/visualize/legacy', + 'plugins/kibana/dashboard/legacy', + ], savedObjectTypes: ['plugins/kibana/dashboard/saved_dashboard/saved_dashboard_register'], app: { id: 'kibana', @@ -78,6 +84,7 @@ export default function(kibana) { order: -1003, url: `${kbnBaseUrl}#/discover`, euiIconType: 'discoverApp', + category: DEFAULT_APP_CATEGORIES.analyze, }, { id: 'kibana:visualize', @@ -87,6 +94,7 @@ export default function(kibana) { order: -1002, url: `${kbnBaseUrl}#/visualize`, euiIconType: 'visualizeApp', + category: DEFAULT_APP_CATEGORIES.analyze, }, { id: 'kibana:dashboard', @@ -102,6 +110,7 @@ export default function(kibana) { // to determine what url to use for the app link. subUrlBase: `${kbnBaseUrl}#/dashboard`, euiIconType: 'dashboardApp', + category: DEFAULT_APP_CATEGORIES.analyze, }, { id: 'kibana:dev_tools', @@ -111,16 +120,18 @@ export default function(kibana) { order: 9001, url: '/app/kibana#/dev_tools', euiIconType: 'devToolsApp', + category: DEFAULT_APP_CATEGORIES.management, }, { - id: 'kibana:management', + id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Management', + defaultMessage: 'Stack Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, euiIconType: 'managementApp', linkToLastSubUrl: false, + category: DEFAULT_APP_CATEGORIES.management, }, ], diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js index 160adcc5b63f1b..9b45217287dc8c 100644 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ b/src/legacy/core_plugins/kibana/public/.eslintrc.js @@ -51,6 +51,7 @@ function buildRestrictedPaths(shimmedPlugins) { from: [ `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`, + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`, ], allowSameFolder: false, errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts index ab8dfe81163e48..2b992f95695f38 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export { getAppStateMock } from './get_app_state_mock'; export { getSavedDashboardMock } from './get_saved_dashboard_mock'; export { getEmbeddableFactoryMock } from './get_embeddable_factories_mock'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts index fd39f28a7673a1..4a8decab6b00e0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -17,40 +17,12 @@ * under the License. */ -import { npSetup, npStart, legacyChrome } from './legacy_imports'; -import { DashboardPlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { start as data } from '../../../data/public/legacy'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; -import './saved_dashboard/saved_dashboard_register'; -import './dashboard_config'; +import { PluginInitializerContext } from 'kibana/public'; +import { DashboardPlugin } from './plugin'; export * from './np_ready/dashboard_constants'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await legacyChrome.dangerouslyGetActiveInjector(); - - return { - dashboardConfig: injector.get('dashboardConfig'), - }; -} - -(async () => { - const instance = new DashboardPlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - getAngularDependencies, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - data, - npData: npStart.plugins.data, - embeddables, - navigation: npStart.plugins.navigation, - }); -})(); +// Core will be looking for this when loading our plugin in the new platform +export const plugin = (context: PluginInitializerContext) => { + return new DashboardPlugin(); +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts new file mode 100644 index 00000000000000..068a8378f936a3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart, legacyChrome } from './legacy_imports'; +import { LegacyAngularInjectedDependencies } from './plugin'; +import { start as data } from '../../../data/public/legacy'; +import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; +import './saved_dashboard/saved_dashboard_register'; +import './dashboard_config'; +import { plugin } from './index'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularDependencies(): Promise { + const injector = await legacyChrome.dangerouslyGetActiveInjector(); + + return { + dashboardConfig: injector.get('dashboardConfig'), + }; +} + +(async () => { + const instance = plugin({} as PluginInitializerContext); + instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + getAngularDependencies, + }, + }); + instance.start(npStart.core, { + ...npStart.plugins, + data, + npData: npStart.plugins.data, + embeddables, + navigation: npStart.plugins.navigation, + }); +})(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index b44d1993db23a4..244a58e8a65e5f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -28,8 +28,6 @@ import chrome from 'ui/chrome'; export const legacyChrome = chrome; export { State } from 'ui/state_management/state'; -export { AppState } from 'ui/state_management/app_state'; -export { AppStateClass } from 'ui/state_management/app_state'; export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { IPrivate } from 'ui/private'; @@ -45,8 +43,6 @@ export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore -export { AppStateProvider } from 'ui/state_management/app_state'; -// @ts-ignore export { PrivateProvider } from 'ui/private/private'; // @ts-ignore export { EventsProvider } from 'ui/events'; @@ -60,9 +56,7 @@ export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; // @ts-ignore export { confirmModalFactory } from 'ui/modals/confirm_modal'; export { configureAppAngularModule } from 'ui/legacy_compat'; -export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; -export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { IInjector } from 'ui/chrome'; export { SavedObjectLoader } from 'ui/saved_objects'; export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 7f7bf7cf47bdaf..429a7f7279996f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -31,7 +31,6 @@ import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { GlobalStateProvider, StateManagementConfigProvider, - AppStateProvider, PrivateProvider, EventsProvider, PersistedState, @@ -155,12 +154,6 @@ function createLocalStateModule() { 'app/dashboard/Promise', 'app/dashboard/PersistedState', ]) - .factory('AppState', function(Private: any) { - return Private(AppStateProvider); - }) - .service('getAppState', function(Private: any) { - return Private(AppStateProvider).getAppState; - }) .service('globalState', function(Private: any) { return Private(GlobalStateProvider); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index e9fdc335ba5729..f56990ae82e560 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -20,12 +20,7 @@ import moment from 'moment'; import { Subscription } from 'rxjs'; -import { - AppStateClass as TAppStateClass, - AppState as TAppState, - IInjector, - KbnUrl, -} from '../legacy_imports'; +import { IInjector } from '../legacy_imports'; import { ViewMode } from '../../../../embeddable_api/public/np_ready/public'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; @@ -43,7 +38,7 @@ import { RenderDeps } from './application'; export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; - appState: TAppState; + appState: DashboardAppState; screenTitle: string; model: { query: Query; @@ -60,7 +55,6 @@ export interface DashboardAppScope extends ng.IScope { refreshInterval: any; panels: SavedDashboardPanel[]; indexPatterns: IIndexPattern[]; - $evalAsync: any; dashboardViewMode: ViewMode; expandedPanel?: string; getShouldShowEditHelp: () => boolean; @@ -91,8 +85,6 @@ export interface DashboardAppScope extends ng.IScope { export function initDashboardAppDirective(app: any, deps: RenderDeps) { app.directive('dashboardApp', function($injector: IInjector) { - const AppState = $injector.get>('AppState'); - const kbnUrl = $injector.get('kbnUrl'); const confirmModal = $injector.get('confirmModal'); const config = deps.uiSettings; @@ -105,17 +97,13 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $routeParams: { id?: string; }, - getAppState: any, globalState: any ) => new DashboardAppController({ $route, $scope, $routeParams, - getAppState, globalState, - kbnUrl, - AppStateClass: AppState, config, confirmModal, indexPatterns: deps.npDataStart.indexPatterns, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 812c3b588a1c8f..4da445166df45d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -17,57 +17,55 @@ * under the License. */ -import _ from 'lodash'; +import _, { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; import angular from 'angular'; -import { uniq } from 'lodash'; import { Subscription } from 'rxjs'; +import { createHashHistory } from 'history'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; import { - subscribeWithScope, ConfirmationButtonTypes, - showSaveModal, - SaveResult, migrateLegacyQuery, - State, - AppStateClass as TAppStateClass, - KbnUrl, SavedObjectSaveOpts, - unhashUrl, + SaveResult, + showSaveModal, + State, + subscribeWithScope, } from '../legacy_imports'; import { FilterStateManager } from '../../../../data/public'; import { + esFilters, IndexPattern, + IndexPatternsContract, Query, SavedQuery, - IndexPatternsContract, } from '../../../../../../plugins/data/public'; import { - DashboardContainer, DASHBOARD_CONTAINER_TYPE, + DashboardContainer, DashboardContainerFactory, DashboardContainerInput, DashboardPanelState, } from '../../../../dashboard_embeddable_container/public/np_ready/public'; import { - isErrorEmbeddable, + EmbeddableFactoryNotFoundError, ErrorEmbeddable, - ViewMode, + isErrorEmbeddable, openAddPanelFlyout, - EmbeddableFactoryNotFoundError, + ViewMode, } from '../../../../embeddable_api/public/np_ready/public'; -import { DashboardAppState, NavAction, ConfirmModalFn, SavedDashboardPanel } from './types'; +import { ConfirmModalFn, NavAction, SavedDashboardPanel } from './types'; import { showOptionsPopover } from './top_nav/show_options_popover'; import { DashboardSaveModal } from './top_nav/save_modal'; import { showCloneModal } from './top_nav/show_clone_modal'; import { saveDashboard } from './lib'; import { DashboardStateManager } from './dashboard_state_manager'; -import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; +import { createDashboardEditUrl, DashboardConstants } from './dashboard_constants'; import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; @@ -78,17 +76,15 @@ import { SavedObjectFinderProps, SavedObjectFinderUi, } from '../../../../../../plugins/kibana_react/public'; +import { removeQueryParam, unhashUrl } from '../../../../../../plugins/kibana_utils/public'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; $route: any; $routeParams: any; - getAppState: any; globalState: State; indexPatterns: IndexPatternsContract; dashboardConfig: any; - kbnUrl: KbnUrl; - AppStateClass: TAppStateClass; config: any; confirmModal: ConfirmModalFn; } @@ -103,12 +99,9 @@ export class DashboardAppController { $scope, $route, $routeParams, - getAppState, globalState, dashboardConfig, localStorage, - kbnUrl, - AppStateClass, indexPatterns, config, confirmModal, @@ -124,7 +117,6 @@ export class DashboardAppController { }, core: { notifications, overlays, chrome, injectedMetadata, uiSettings, savedObjects, http }, }: DashboardAppControllerDependencies) { - new FilterStateManager(globalState, getAppState, filterManager); const queryFilter = filterManager; let lastReloadRequestTime = 0; @@ -134,14 +126,30 @@ export class DashboardAppController { chrome.docTitle.change(dash.title); } + const history = createHashHistory(); const dashboardStateManager = new DashboardStateManager({ savedDashboard: dash, - AppStateClass, + useHashedUrl: config.get('state:storeInSessionStorage'), hideWriteControls: dashboardConfig.getHideWriteControls(), kibanaVersion: injectedMetadata.getKibanaVersion(), + history, }); - $scope.appState = dashboardStateManager.getAppState(); + const filterStateManager = new FilterStateManager( + globalState, + () => { + // Temporary AppState replacement + return { + set filters(_filters: esFilters.Filter[]) { + dashboardStateManager.setFilters(_filters); + }, + get filters() { + return dashboardStateManager.appState.filters; + }, + }; + }, + filterManager + ); // The hash check is so we only update the time filter on dashboard open, not during // normal cross app navigation. @@ -316,8 +324,8 @@ export class DashboardAppController { dirty = true; } + dashboardStateManager.handleDashboardContainerChanges(container); $scope.$evalAsync(() => { - dashboardStateManager.handleDashboardContainerChanges(container); if (dirty) { updateState(); } @@ -337,8 +345,8 @@ export class DashboardAppController { const type = $routeParams[DashboardConstants.ADD_EMBEDDABLE_TYPE]; const id = $routeParams[DashboardConstants.ADD_EMBEDDABLE_ID]; container.addSavedObjectEmbeddable(type, id); - kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_TYPE); - kbnUrl.removeParam(DashboardConstants.ADD_EMBEDDABLE_ID); + removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_TYPE); + removeQueryParam(history, DashboardConstants.ADD_EMBEDDABLE_ID); } } @@ -409,7 +417,9 @@ export class DashboardAppController { key ]; if (!_.isEqual(containerValue, appStateValue)) { - (differences as { [key: string]: unknown })[key] = appStateValue; + // cloneDeep hack is needed, as there are multiple place, where container's input mutated, + // but values from appStateValue are deeply frozen, as they can't be mutated directly + (differences as { [key: string]: unknown })[key] = _.cloneDeep(appStateValue); } }); @@ -563,11 +573,6 @@ export class DashboardAppController { ); function updateViewMode(newMode: ViewMode) { - $scope.topNavMenu = getTopNavConfig( - newMode, - navActions, - dashboardConfig.getHideWriteControls() - ); // eslint-disable-line no-use-before-define dashboardStateManager.switchViewMode(newMode); } @@ -583,17 +588,28 @@ export class DashboardAppController { function revertChangesAndExitEditMode() { dashboardStateManager.resetState(); - kbnUrl.change( - dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL - ); // This is only necessary for new dashboards, which will default to Edit mode. updateViewMode(ViewMode.VIEW); + // Angular's $location skips this update because of history updates from syncState which happen simultaneously + // when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it, + // the update is considered outdated and angular skips it + // so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues + dashboardStateManager.changeDashboardUrl( + dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL + ); + // We need to do a hard reset of the timepicker. appState will not reload like // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on // reload will cause it not to sync. if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); + // have to use $evalAsync here until '_g' is migrated from $location to state sync utility ('history') + // When state sync utility changes url, angular's $location is missing it's own updates which happen during the same digest cycle + // temporary solution is to delay $location updates to next digest cycle + // unfortunately, these causes 2 browser history entries, but this is temporary and will be fixed after migrating '_g' to state_sync utilities + $scope.$evalAsync(() => { + dashboardStateManager.syncTimefilterWithDashboard(timefilter); + }); } } @@ -645,7 +661,11 @@ export class DashboardAppController { }); if (dash.id !== $routeParams.id) { - kbnUrl.change(createDashboardEditUrl(dash.id)); + // Angular's $location skips this update because of history updates from syncState which happen simultaneously + // when calling kbnUrl.change() angular schedules url update and when angular finally starts to process it, + // the update is considered outdated and angular skips it + // so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues + dashboardStateManager.changeDashboardUrl(createDashboardEditUrl(dash.id)); } else { chrome.docTitle.change(dash.lastSavedTitle); updateViewMode(ViewMode.VIEW); @@ -847,6 +867,15 @@ export class DashboardAppController { }); }); + dashboardStateManager.registerChangeListener(() => { + // view mode could have changed, so trigger top nav update + $scope.topNavMenu = getTopNavConfig( + dashboardStateManager.getViewMode(), + navActions, + dashboardConfig.getHideWriteControls() + ); + }); + $scope.$on('$destroy', () => { updateSubscription.unsubscribe(); visibleSubscription.unsubscribe(); @@ -862,6 +891,9 @@ export class DashboardAppController { if (dashboardContainer) { dashboardContainer.destroy(); } + if (filterStateManager) { + filterStateManager.destroy(); + } }); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index 4d5101e1f9e5f7..8806684aab13cf 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -18,15 +18,17 @@ */ import './np_core.test.mocks'; +import { createBrowserHistory } from 'history'; import { DashboardStateManager } from './dashboard_state_manager'; -import { getAppStateMock, getSavedDashboardMock } from '../__tests__'; -import { AppStateClass } from '../legacy_imports'; -import { DashboardAppState } from './types'; -import { TimeRange, TimefilterContract, InputTimeRange } from 'src/plugins/data/public'; +import { getSavedDashboardMock } from '../__tests__'; +import { InputTimeRange, TimefilterContract, TimeRange } from 'src/plugins/data/public'; import { ViewMode } from 'src/plugins/embeddable/public'; -jest.mock('ui/state_management/state', () => ({ - State: {}, +jest.mock('ui/agg_types', () => ({ + aggTypes: { + metrics: [], + buckets: [], + }, })); describe('DashboardState', function() { @@ -46,9 +48,10 @@ describe('DashboardState', function() { function initDashboardState() { dashboardState = new DashboardStateManager({ savedDashboard, - AppStateClass: getAppStateMock() as AppStateClass, + useHashedUrl: false, hideWriteControls: false, kibanaVersion: '7.0.0', + history: createBrowserHistory(), }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index 6df18757da6f51..451e7c8ff96db0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -19,20 +19,16 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; - +import { History } from 'history'; +import { Subscription } from 'rxjs'; import { Moment } from 'moment'; import { DashboardContainer } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; import { ViewMode } from '../../../../../../plugins/embeddable/public'; +import { migrateLegacyQuery } from '../legacy_imports'; import { - stateMonitorFactory, - StateMonitor, - AppStateClass as TAppStateClass, - migrateLegacyQuery, -} from '../legacy_imports'; -import { - Query, esFilters, + Query, TimefilterContract as Timefilter, } from '../../../../../../plugins/data/public'; @@ -41,7 +37,20 @@ import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_o import { FilterUtils } from './lib/filter_utils'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; -import { SavedDashboardPanel, DashboardAppState, DashboardAppStateDefaults } from './types'; +import { + DashboardAppState, + DashboardAppStateDefaults, + DashboardAppStateTransitions, + SavedDashboardPanel, +} from './types'; +import { + createKbnUrlStateStorage, + createStateContainer, + IKbnUrlStateStorage, + ISyncStateRef, + ReduxLikeStateContainer, + syncState, +} from '../../../../../../plugins/kibana_utils/public'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the @@ -51,7 +60,6 @@ import { SavedDashboardPanel, DashboardAppState, DashboardAppStateDefaults } fro */ export class DashboardStateManager { public savedDashboard: SavedObjectDashboard; - public appState: DashboardAppState; public lastSavedDashboardFilters: { timeTo?: string | Moment; timeFrom?: string | Moment; @@ -63,38 +71,78 @@ export class DashboardStateManager { private kibanaVersion: string; public isDirty: boolean; private changeListeners: Array<(status: { dirty: boolean }) => void>; - private stateMonitor: StateMonitor; + + public get appState(): DashboardAppState { + return this.stateContainer.get(); + } + + private readonly stateContainer: ReduxLikeStateContainer< + DashboardAppState, + DashboardAppStateTransitions + >; + private readonly stateContainerChangeSub: Subscription; + private readonly STATE_STORAGE_KEY = '_a'; + private readonly kbnUrlStateStorage: IKbnUrlStateStorage; + private readonly stateSyncRef: ISyncStateRef; + private readonly history: History; /** * * @param savedDashboard - * @param AppState The AppState class to use when instantiating a new AppState instance. * @param hideWriteControls true if write controls should be hidden. + * @param kibanaVersion current kibanaVersion + * @param */ constructor({ savedDashboard, - AppStateClass, hideWriteControls, kibanaVersion, + useHashedUrl, + history, }: { savedDashboard: SavedObjectDashboard; - AppStateClass: TAppStateClass; hideWriteControls: boolean; kibanaVersion: string; + useHashedUrl: boolean; + history: History; }) { + this.history = history; this.kibanaVersion = kibanaVersion; this.savedDashboard = savedDashboard; this.hideWriteControls = hideWriteControls; - this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); + // get state defaults from saved dashboard, make sure it is migrated + this.stateDefaults = migrateAppState( + getAppStateDefaults(this.savedDashboard, this.hideWriteControls), + kibanaVersion + ); + + this.kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: useHashedUrl, history }); - this.appState = new AppStateClass(this.stateDefaults); + // setup initial state by merging defaults with state from url + // also run migration, as state in url could be of older version + const initialState = migrateAppState( + { + ...this.stateDefaults, + ...this.kbnUrlStateStorage.get(this.STATE_STORAGE_KEY), + }, + kibanaVersion + ); - // Initializing appState does two things - first it translates the defaults into AppState, second it updates - // appState based on the URL (the url trumps the defaults). This means if we update the state format at all and - // want to handle BWC, we must not only migrate the data stored with saved Dashboard, but also any old state in the - // url. - migrateAppState(this.appState, kibanaVersion); + // setup state container using initial state both from defaults and from url + this.stateContainer = createStateContainer( + initialState, + { + set: state => (prop, value) => ({ ...state, [prop]: value }), + setOption: state => (option, value) => ({ + ...state, + options: { + ...state.options, + [option]: value, + }, + }), + } + ); this.isDirty = false; @@ -104,29 +152,35 @@ export class DashboardStateManager { // in the 'lose changes' warning message. this.lastSavedDashboardFilters = this.getFilterState(); - /** - * Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState. - */ - this.stateMonitor = stateMonitorFactory.create( - this.appState, - this.stateDefaults - ); - - this.stateMonitor.ignoreProps('viewMode'); - // Filters need to be compared manually because they sometimes have a $$hashkey stored on the object. - this.stateMonitor.ignoreProps('filters'); - // Query needs to be compared manually because saved legacy queries get migrated in app state automatically - this.stateMonitor.ignoreProps('query'); + this.changeListeners = []; - this.stateMonitor.onChange((status: { dirty: boolean }) => { - this.isDirty = status.dirty; + this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { + this.isDirty = this.checkIsDirty(); + this.changeListeners.forEach(listener => listener({ dirty: this.isDirty })); }); - this.changeListeners = []; - - this.stateMonitor.onChange((status: { dirty: boolean }) => { - this.changeListeners.forEach(listener => listener(status)); + // make sure url ('_a') matches initial state + this.kbnUrlStateStorage.set(this.STATE_STORAGE_KEY, initialState, { replace: true }); + + // setup state syncing utils. state container will be synched with url into `this.STATE_STORAGE_KEY` query param + this.stateSyncRef = syncState({ + storageKey: this.STATE_STORAGE_KEY, + stateContainer: { + ...this.stateContainer, + set: (state: DashboardAppState | null) => { + // sync state required state container to be able to handle null + // overriding set() so it could handle null coming from url + this.stateContainer.set({ + ...this.stateDefaults, + ...state, + }); + }, + }, + stateStorage: this.kbnUrlStateStorage, }); + + // actually start syncing state with container + this.stateSyncRef.start(); } public registerChangeListener(callback: (status: { dirty: boolean }) => void) { @@ -172,7 +226,7 @@ export class DashboardStateManager { }); if (dirty) { - this.appState.panels = Object.values(convertedPanelStateMap); + this.stateContainer.transitions.set('panels', Object.values(convertedPanelStateMap)); } if (input.isFullScreenMode !== this.getFullScreenMode()) { @@ -184,7 +238,6 @@ export class DashboardStateManager { } this.changeListeners.forEach(listener => listener({ dirty })); - this.saveState(); } public getFullScreenMode() { @@ -192,8 +245,11 @@ export class DashboardStateManager { } public setFullScreenMode(fullScreenMode: boolean) { - this.appState.fullScreenMode = fullScreenMode; - this.saveState(); + this.stateContainer.transitions.set('fullScreenMode', fullScreenMode); + } + + public setFilters(filters: esFilters.Filter[]) { + this.stateContainer.transitions.set('filters', filters); } /** @@ -210,7 +266,10 @@ export class DashboardStateManager { // The right way to fix this might be to ensure the defaults object stored on state is a deep // clone, but given how much code uses the state object, I determined that to be too risky of a change for // now. TODO: revisit this! - this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls); + this.stateDefaults = migrateAppState( + getAppStateDefaults(this.savedDashboard, this.hideWriteControls), + this.kibanaVersion + ); // The original query won't be restored by the above because the query on this.savedDashboard is applied // in place in order for it to affect the visualizations. this.stateDefaults.query = this.lastSavedDashboardFilters.query; @@ -218,9 +277,7 @@ export class DashboardStateManager { this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; this.isDirty = false; - this.appState.setDefaults(this.stateDefaults); - this.appState.reset(); - this.stateMonitor.setInitialState(this.appState.toJSON()); + this.stateContainer.set(this.stateDefaults); } /** @@ -252,31 +309,28 @@ export class DashboardStateManager { } public setDescription(description: string) { - this.appState.description = description; - this.saveState(); + this.stateContainer.transitions.set('description', description); } public setTitle(title: string) { - this.appState.title = title; this.savedDashboard.title = title; - this.saveState(); + this.stateContainer.transitions.set('title', title); } public getAppState() { - return this.appState; + return this.stateContainer.get(); } public getQuery(): Query { - return migrateLegacyQuery(this.appState.query); + return migrateLegacyQuery(this.stateContainer.get().query); } public getSavedQueryId() { - return this.appState.savedQuery; + return this.stateContainer.get().savedQuery; } public setSavedQueryId(id?: string) { - this.appState.savedQuery = id; - this.saveState(); + this.stateContainer.transitions.set('savedQuery', id); } public getUseMargins() { @@ -287,8 +341,7 @@ export class DashboardStateManager { } public setUseMargins(useMargins: boolean) { - this.appState.options.useMargins = useMargins; - this.saveState(); + this.stateContainer.transitions.setOption('useMargins', useMargins); } public getHidePanelTitles() { @@ -296,8 +349,7 @@ export class DashboardStateManager { } public setHidePanelTitles(hidePanelTitles: boolean) { - this.appState.options.hidePanelTitles = hidePanelTitles; - this.saveState(); + this.stateContainer.transitions.setOption('hidePanelTitles', hidePanelTitles); } public getTimeRestore() { @@ -305,8 +357,7 @@ export class DashboardStateManager { } public setTimeRestore(timeRestore: boolean) { - this.appState.timeRestore = timeRestore; - this.saveState(); + this.stateContainer.transitions.set('timeRestore', timeRestore); } public getIsTimeSavedWithDashboard() { @@ -397,7 +448,6 @@ export class DashboardStateManager { (panel: SavedDashboardPanel) => panel.panelIndex === panelIndex ); Object.assign(foundPanel, panelAttributes); - this.saveState(); return foundPanel; } @@ -456,15 +506,37 @@ export class DashboardStateManager { } /** - * Saves the current application state to the URL. + * Synchronously writes current state to url + * returned boolean indicates whether the update happened and if history was updated */ - public saveState() { - this.appState.save(); + private saveState({ replace }: { replace: boolean }): boolean { + // schedules setting current state to url + this.kbnUrlStateStorage.set( + this.STATE_STORAGE_KEY, + this.stateContainer.get() + ); + // immediately forces scheduled updates and changes location + return this.kbnUrlStateStorage.flush({ replace }); + } + + // TODO: find nicer solution for this + // this function helps to make just 1 browser history update, when we imperatively changing the dashboard url + // It could be that there is pending *dashboardStateManager* updates, which aren't flushed yet to the url. + // So to prevent 2 browser updates: + // 1. Force flush any pending state updates (syncing state to query) + // 2. If url was updated, then apply path change with replace + public changeDashboardUrl(pathname: string) { + // synchronously persist current state to url with push() + const updated = this.saveState({ replace: false }); + // change pathname + this.history[updated ? 'replace' : 'push']({ + ...this.history.location, + pathname, + }); } public setQuery(query: Query) { - this.appState.query = query; - this.saveState(); + this.stateContainer.transitions.set('query', query); } /** @@ -472,24 +544,33 @@ export class DashboardStateManager { * @param filter An array of filter bar filters. */ public applyFilters(query: Query, filters: esFilters.Filter[]) { - this.appState.query = query; this.savedDashboard.searchSource.setField('query', query); this.savedDashboard.searchSource.setField('filter', filters); - this.saveState(); + this.stateContainer.transitions.set('query', query); } public switchViewMode(newMode: ViewMode) { - this.appState.viewMode = newMode; - this.saveState(); + this.stateContainer.transitions.set('viewMode', newMode); } /** * Destroys and cleans up this object when it's no longer used. */ public destroy() { - if (this.stateMonitor) { - this.stateMonitor.destroy(); - } + this.stateContainerChangeSub.unsubscribe(); this.savedDashboard.destroy(); + if (this.stateSyncRef) { + this.stateSyncRef.stop(); + } + } + + private checkIsDirty() { + // Filters need to be compared manually because they sometimes have a $$hashkey stored on the object. + // Query needs to be compared manually because saved legacy queries get migrated in app state automatically + const propsToIgnore: Array = ['viewMode', 'filters', 'query']; + + const initial = _.omit(this.stateDefaults, propsToIgnore); + const current = _.omit(this.stateContainer.get(), propsToIgnore); + return !_.isEqual(initial, current); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 540bfcf5aa6847..7dc408ea4b8013 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -35,6 +35,7 @@ import { import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; import { syncOnMount } from './global_state_sync'; +import { createHashHistory } from 'history'; export function initDashboardApp(app, deps) { initDashboardAppDirective(app, deps); @@ -190,7 +191,7 @@ export function initDashboardApp(app, deps) { template: dashboardTemplate, controller: createNewDashboardCtrl, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, AppState) { + dash: function($rootScope, $route, redirectWhenMissing, kbnUrl) { const id = $route.current.params.id; return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl) @@ -216,8 +217,13 @@ export function initDashboardApp(app, deps) { // Preserve BWC of v5.3.0 links for new, unsaved dashboards. // See https://github.com/elastic/kibana/issues/10951 for more context. if (error instanceof SavedObjectNotFound && id === 'create') { - // Note "new AppState" is necessary so the state in the url is preserved through the redirect. - kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState()); + // Note preserve querystring part is necessary so the state is preserved through the redirect. + const history = createHashHistory(); + history.replace({ + ...history.location, // preserve query, + pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, + }); + deps.toastNotifications.addWarning( i18n.translate('kbn.dashboard.urlWasRemovedInSixZeroWarningMessage', { defaultMessage: diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts index 4aa2461bb65930..73336ec951894e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.test.ts @@ -23,7 +23,6 @@ import { SavedDashboardPanel } from '../types'; import { migrateAppState } from './migrate_app_state'; test('migrate app state from 6.0', async () => { - const mockSave = jest.fn(); const appState = { uiState: { 'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } }, @@ -39,11 +38,8 @@ test('migrate app state from 6.0', async () => { type: 'visualization', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect(appState.uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -54,12 +50,10 @@ test('migrate app state from 6.0', async () => { expect(newPanel.gridData.y).toBe(0); expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); - expect(mockSave).toBeCalledTimes(1); }); test('migrate sort from 6.1', async () => { const TARGET_VERSION = '8.0'; - const mockSave = jest.fn(); const appState = { uiState: { 'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } }, @@ -76,12 +70,9 @@ test('migrate sort from 6.1', async () => { sort: 'sort', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, useMargins: false, }; - migrateAppState(appState, TARGET_VERSION); + migrateAppState(appState as any, TARGET_VERSION); expect(appState.uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -91,11 +82,9 @@ test('migrate sort from 6.1', async () => { expect((newPanel.embeddableConfig as any).sort).toBe('sort'); expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)'); - expect(mockSave).toBeCalledTimes(1); }); test('migrates 6.0 even when uiState does not exist', async () => { - const mockSave = jest.fn(); const appState = { panels: [ { @@ -109,11 +98,8 @@ test('migrates 6.0 even when uiState does not exist', async () => { sort: 'sort', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -122,11 +108,9 @@ test('migrates 6.0 even when uiState does not exist', async () => { expect((newPanel as any).sort).toBeUndefined(); expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect(mockSave).toBeCalledTimes(1); }); test('6.2 migration adjusts w & h without margins', async () => { - const mockSave = jest.fn(); const appState = { panels: [ { @@ -143,12 +127,9 @@ test('6.2 migration adjusts w & h without margins', async () => { version: '6.2.0', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, useMargins: false, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -159,11 +140,9 @@ test('6.2 migration adjusts w & h without margins', async () => { expect((newPanel as any).sort).toBeUndefined(); expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect(mockSave).toBeCalledTimes(1); }); test('6.2 migration adjusts w & h with margins', async () => { - const mockSave = jest.fn(); const appState = { panels: [ { @@ -180,12 +159,9 @@ test('6.2 migration adjusts w & h with margins', async () => { version: '6.2.0', }, ], - translateHashToRison: () => 'a', - getQueryParamName: () => 'a', - save: mockSave, useMargins: true, }; - migrateAppState(appState, '8.0'); + migrateAppState(appState as any, '8.0'); expect((appState as any).uiState).toBeUndefined(); const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel; @@ -196,5 +172,4 @@ test('6.2 migration adjusts w & h with margins', async () => { expect((newPanel as any).sort).toBeUndefined(); expect((newPanel.embeddableConfig as any).sort).toBe('sort'); - expect(mockSave).toBeCalledTimes(1); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts index 4083900c7ede7e..0cd958ced0eb1c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/migrate_app_state.ts @@ -28,6 +28,7 @@ import { SavedDashboardPanel630, SavedDashboardPanel640To720, SavedDashboardPanel620, + SavedDashboardPanel, } from '../types'; import { migratePanelsTo730 } from '../../migrations/migrate_to_730_panels'; @@ -37,9 +38,9 @@ import { migratePanelsTo730 } from '../../migrations/migrate_to_730_panels'; * Once we hit a major version, we can remove support for older style URLs and get rid of this logic. */ export function migrateAppState( - appState: { [key: string]: unknown } | DashboardAppState, + appState: { [key: string]: unknown } & DashboardAppState, kibanaVersion: string -) { +): DashboardAppState { if (!appState.panels) { throw new Error( i18n.translate('kbn.dashboard.panel.invalidData', { @@ -76,11 +77,11 @@ export function migrateAppState( | SavedDashboardPanel640To720 >, kibanaVersion, - appState.useMargins, - appState.uiState - ); + appState.useMargins as boolean, + appState.uiState as Record> + ) as SavedDashboardPanel[]; delete appState.uiState; - - appState.save(); } + + return appState; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts index 691c87122564f6..d80208ce27ffe4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/save_dashboard.ts @@ -36,16 +36,19 @@ export function saveDashboard( dashboardStateManager: DashboardStateManager, saveOptions: SavedObjectSaveOpts ): Promise { - dashboardStateManager.saveState(); - const savedDashboard = dashboardStateManager.savedDashboard; const appState = dashboardStateManager.appState; updateSavedDashboard(savedDashboard, appState, timeFilter, toJson); return savedDashboard.save(saveOptions).then((id: string) => { - dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); + if (id) { + // reset state only when save() was successful + // e.g. save() could be interrupted if title is duplicated and not confirmed + dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); + dashboardStateManager.resetState(); + } + return id; }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts index 2072b5d4f6eb06..ec8073c0f72f74 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/update_saved_dashboard.ts @@ -19,13 +19,13 @@ import _ from 'lodash'; import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public'; -import { AppState } from '../../legacy_imports'; import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../../saved_dashboard/saved_dashboard'; +import { DashboardAppState } from '../types'; export function updateSavedDashboard( savedDashboard: SavedObjectDashboard, - appState: AppState, + appState: DashboardAppState, timeFilter: TimefilterContract, toJson: (object: T) => string ) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts index e3eb25a208856f..3151fbf821b9fe 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts @@ -18,7 +18,6 @@ */ import { ViewMode } from 'src/plugins/embeddable/public'; -import { AppState } from '../legacy_imports'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -93,11 +92,7 @@ export type SavedDashboardPanelTo60 = Pick< readonly type: string; }; -export type DashboardAppStateDefaults = DashboardAppStateParameters & { - description?: string; -}; - -export interface DashboardAppStateParameters { +export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; title: string; @@ -113,9 +108,24 @@ export interface DashboardAppStateParameters { savedQuery?: string; } -// This could probably be improved if we flesh out AppState more... though AppState will be going away -// so maybe not worth too much time atm. -export type DashboardAppState = DashboardAppStateParameters & AppState; +export type DashboardAppStateDefaults = DashboardAppState & { + description?: string; +}; + +export interface DashboardAppStateTransitions { + set: ( + state: DashboardAppState + ) => ( + prop: T, + value: DashboardAppState[T] + ) => DashboardAppState; + setOption: ( + state: DashboardAppState + ) => ( + prop: T, + value: DashboardAppState['options'][T] + ) => DashboardAppState; +} export interface SavedDashboardPanelMap { [key: string]: SavedDashboardPanel; @@ -139,18 +149,3 @@ export type ConfirmModalFn = ( title: string; } ) => void; - -export type AddFilterFn = ( - { - field, - value, - operator, - index, - }: { - field: string; - value: string; - operator: string; - index: string; - }, - appState: AppState -) => void; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 18d83595f8fa30..6ffda87ac2be8d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; // Load the kibana app dependencies. diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js index 378a9e93256557..f302d684135f6c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import ngMock from 'ng_mock'; import { fieldCalculator } from '../../np_ready/components/field_chooser/lib/field_calculator'; import expect from '@kbn/expect'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index 5f6898ae2bd164..f74e145865475d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -23,7 +23,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesHitsProvider from 'fixtures/hits'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { SimpleSavedObject } from '../../../../../../../core/public'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js index b57f452b637afa..6b97da79fc5893 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import _ from 'lodash'; import ngMock from 'ng_mock'; import 'ui/private'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import hits from 'fixtures/real_hits'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js index 012f2b6061ee42..c19e033ccb72df 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js @@ -24,7 +24,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { getFakeRow, getFakeRowVals } from 'fixtures/fake_row'; import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('Doc Table', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js index 90614cf3c132c2..f2acbf363d825d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js @@ -22,7 +22,7 @@ import ngMock from 'ng_mock'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; import { createIndexPatternsStub } from '../../np_ready/angular/context/api/__tests__/_stubs'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { npStart } from 'ui/new_platform'; describe('context app', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js index 1ad4bdbea210db..9ba425bb0e489e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js index e9ec2c300faa11..39dde2d8bb7cf0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js index 15f3eefac3fd1a..c05f5b4aff3bc7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.ts b/src/legacy/core_plugins/kibana/public/discover/index.ts index 7bde30e0d0f0ef..d851cb96a18c48 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/index.ts @@ -16,24 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import chrome from 'ui/chrome'; + import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; import { DiscoverPlugin } from './plugin'; +export { createSavedSearchesService } from './saved_searches/saved_searches'; + // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { return new DiscoverPlugin(); }; - -// Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file -export const pluginInstance = plugin({} as PluginInitializerContext); -export const setup = pluginInstance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - chrome, - }, -}); -export const start = pluginInstance.start(npStart.core, npStart.plugins); - -export { createSavedSearchesService } from './saved_searches/saved_searches'; diff --git a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js b/src/legacy/core_plugins/kibana/public/discover/legacy.ts similarity index 62% rename from src/legacy/core_plugins/state_session_storage_redirect/public/index.js rename to src/legacy/core_plugins/kibana/public/discover/legacy.ts index 701a5736c7d3b8..2ec64177156f9e 100644 --- a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/legacy.ts @@ -16,25 +16,17 @@ * specific language governing permissions and limitations * under the License. */ - import chrome from 'ui/chrome'; -import { hashUrl } from '../../../../plugins/kibana_utils/public'; -import uiRoutes from 'ui/routes'; -import { fatalError } from 'ui/notify'; - -uiRoutes.enable(); -uiRoutes.when('/', { - resolve: { - url: function(AppState, globalState, $window) { - const redirectUrl = chrome.getInjected('redirectUrl'); - try { - const hashedUrl = hashUrl(redirectUrl); - const url = chrome.addBasePath(hashedUrl); +import { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { plugin } from './index'; - $window.location = url; - } catch (e) { - fatalError(e); - } - }, +// Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file +export const pluginInstance = plugin({} as PluginInitializerContext); +export const setup = pluginInstance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + chrome, }, }); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js index 45ce6cc9d0af2e..debcccebbd11c0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js index 266a505f6be14b..c24b6ac6307ffd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js index e06d414ba260ce..d4c00930c93839 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 32373faba92e2d..cde0b5d27bdc54 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -637,7 +637,7 @@ function discoverController( // fetch data when filters fire fetch event subscriptions.add( - subscribeWithScope($scope, filterManager.getUpdates$(), { + subscribeWithScope($scope, filterManager.getFetches$(), { next: $scope.fetch, }) ); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts index 212fd870a5aeb8..2bbeea9d675c7e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/get_painless_error.ts @@ -25,6 +25,7 @@ export function getPainlessError(error: Error) { error, 'resp.error.root_cause' ); + const message: string = get(error, 'message'); if (!rootCause) { return; @@ -43,6 +44,6 @@ export function getPainlessError(error: Error) { defaultMessage: "Error with Painless scripted field '{script}'.", values: { script }, }), - error: error.message, + error: message, }; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx index d2dda32f318fe5..1aad7e953b8de0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/fetch_error/fetch_error.tsx @@ -39,7 +39,7 @@ const DiscoverFetchError = ({ fetchError }: Props) => { if (fetchError.lang === 'painless') { const { chrome } = getServices(); - const mangagementUrlObj = chrome.navLinks.get('kibana:management'); + const mangagementUrlObj = chrome.navLinks.get('kibana:stack_management'); const managementUrl = mangagementUrlObj ? mangagementUrlObj.url : ''; const url = `${managementUrl}/kibana/index_patterns`; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx index f816b33bcd0ae3..386f405544a61f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx @@ -26,50 +26,48 @@ import { IndexPattern, indexPatterns } from '../../../kibana_services'; jest.mock('ui/new_platform'); -// @ts-ignore const indexPattern = { - fields: { - getByName: (name: string) => { - const fields: { [name: string]: {} } = { - _index: { - name: '_index', - type: 'string', - scripted: false, - filterable: true, - }, - message: { - name: 'message', - type: 'string', - scripted: false, - filterable: false, - }, - extension: { - name: 'extension', - type: 'string', - scripted: false, - filterable: true, - }, - bytes: { - name: 'bytes', - type: 'number', - scripted: false, - filterable: true, - }, - scripted: { - name: 'scripted', - type: 'number', - scripted: true, - filterable: false, - }, - }; - return fields[name]; + fields: [ + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'message', + type: 'string', + scripted: false, + filterable: false, + }, + { + name: 'extension', + type: 'string', + scripted: false, + filterable: true, }, - }, + { + name: 'bytes', + type: 'number', + scripted: false, + filterable: true, + }, + { + name: 'scripted', + type: 'number', + scripted: true, + filterable: false, + }, + ], metaFields: ['_index', '_score'], flattenHit: undefined, formatHit: jest.fn(hit => hit._source), } as IndexPattern; +indexPattern.fields.getByName = (name: string) => { + return indexPattern.fields.find(field => field.name === name); +}; + indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); describe('DocViewTable at Discover', () => { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx index 4bb2f83016c22e..85689768eb88e9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx @@ -17,6 +17,7 @@ * under the License. */ import React, { useState } from 'react'; +import { escapeRegExp } from 'lodash'; import { DocViewTableRow } from './table_row'; import { arrayContainsObjects, trimAngularSpan } from './table_helper'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; @@ -68,11 +69,57 @@ export function DocViewTable({ const displayNoMappingWarning = !mapping(field) && !displayUnderscoreWarning && !isArrayOfObjects; + // Discover doesn't flatten arrays of objects, so for documents with an `object` or `nested` field that + // contains an array, Discover will only detect the top level root field. We want to detect when those + // root fields are `nested` so that we can display the proper icon and label. However, those root + // `nested` fields are not a part of the index pattern. Their children are though, and contain nested path + // info. So to detect nested fields we look through the index pattern for nested children + // whose path begins with the current field. There are edge cases where + // this could incorrectly identify a plain `object` field as `nested`. Say we had the following document + // where `foo` is a plain object field and `bar` is a nested field. + // { + // "foo": [ + // { + // "bar": [ + // { + // "baz": "qux" + // } + // ] + // }, + // { + // "bar": [ + // { + // "baz": "qux" + // } + // ] + // } + // ] + // } + // + // The following code will search for `foo`, find it at the beginning of the path to the nested child field + // `foo.bar.baz` and incorrectly mark `foo` as nested. Any time we're searching for the name of a plain object + // field that happens to match a segment of a nested path, we'll get a false positive. + // We're aware of this issue and we'll have to live with + // it in the short term. The long term fix will be to add info about the `nested` and `object` root fields + // to the index pattern, but that has its own complications which you can read more about in the following + // issue: https://github.com/elastic/kibana/issues/54957 + const isNestedField = + !indexPattern.fields.find(patternField => patternField.name === field) && + !!indexPattern.fields.find(patternField => { + // We only want to match a full path segment + const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)'); + return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); + }); + const fieldType = isNestedField + ? 'nested' + : indexPattern.fields.find(patternField => patternField.name === field)?.type; + return ( )} - + {isCollapsible && ( diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index b2d90f1444654a..27d09a53ba20dc 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -22,11 +22,8 @@ import { npSetup, npStart } from 'ui/new_platform'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -export const trackUiMetric = createUiStatsReporter('Kibana_home'); - /** * Get dependencies relying on the global angular context. * They also have to get resolved together with the legacy imports above @@ -54,9 +51,7 @@ let copiedLegacyCatalogue = false; instance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { - trackUiMetric, metadata: npStart.core.injectedMetadata.getLegacyMetadata(), - METRIC_TYPE, getFeatureCatalogueEntries: async () => { if (!copiedLegacyCatalogue) { const injector = await chrome.dangerouslyGetActiveInjector(); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 0eb55a3902edac..4d9177735556df 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -55,7 +55,6 @@ export interface HomeKibanaServices { savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; banners: OverlayStart['banners']; - METRIC_TYPE: any; trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; getBasePath: () => string; shouldShowTelemetryOptIn: boolean; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index be2ceb66f69d03..27d4f1a8b1c1fa 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -129,8 +129,8 @@ describe('home', () => { test('should not render directory entry when showOnHomePage is false', async () => { const directoryEntry = { - id: 'management', - title: 'Management', + id: 'stack-management', + title: 'Stack Management', description: 'Your center console for managing the Elastic Stack.', icon: 'managementApp', path: 'management_landing_page', diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 28bdab14193c40..55c469fa58fc61 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -25,10 +25,6 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ addBasePath: (path: string) => `root${path}`, trackUiMetric: () => {}, - METRIC_TYPE: { - LOADED: 'loaded', - CLICK: 'click', - }, }), })); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 9bbb7aaceb915b..1b7761d068d2f2 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -35,6 +35,7 @@ import { EuiIcon, EuiPortal, } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../../kibana_services'; @@ -64,17 +65,17 @@ export class Welcome extends React.Component { } private onSampleDataDecline = () => { - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataDecline'); + this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataDecline'); this.props.onSkip(); }; private onSampleDataConfirm = () => { - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataConfirm'); + this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataConfirm'); this.redirecToSampleData(); }; componentDidMount() { - this.services.trackUiMetric(this.services.METRIC_TYPE.LOADED, 'welcomeScreenMount'); + this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); this.props.onOptInSeen(); document.addEventListener('keydown', this.hideOnEsc); } diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 42ab049eb5b3a8..502c8f45646cf0 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -18,11 +18,11 @@ */ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; -import { UiStatsMetricType } from '@kbn/analytics'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { setServices } from './kibana_services'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; +import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Environment, FeatureCatalogueEntry, @@ -41,8 +41,6 @@ export interface HomePluginStartDependencies { export interface HomePluginSetupDependencies { __LEGACY: { - trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; - METRIC_TYPE: any; metadata: { app: unknown; bundleId: string; @@ -59,6 +57,7 @@ export interface HomePluginSetupDependencies { getFeatureCatalogueEntries: () => Promise; getAngularDependencies: () => Promise; }; + usageCollection: UsageCollectionSetup; kibana_legacy: KibanaLegacySetup; } @@ -71,6 +70,7 @@ export class HomePlugin implements Plugin { core: CoreSetup, { kibana_legacy, + usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, }: HomePluginSetupDependencies ) { @@ -78,9 +78,11 @@ export class HomePlugin implements Plugin { id: 'home', title: 'Home', mount: async ({ core: contextCore }, params) => { + const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); const angularDependencies = await getAngularDependencies(); setServices({ ...legacyServices, + trackUiMetric, http: contextCore.http, toastNotifications: core.notifications.toasts, banners: contextCore.overlays.banners, diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 4100ae72058699..bd947b9cb9d7f1 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -47,9 +47,9 @@ import 'uiExports/interpreter'; import 'ui/autoload/all'; import 'ui/kbn_top_nav'; import './home'; -import './discover'; -import './visualize'; -import './dashboard'; +import './discover/legacy'; +import './visualize/legacy'; +import './dashboard/legacy'; import './management'; import './dev_tools'; import 'ui/color_maps'; diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index d62770956b88ef..1305310b6f6151 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -74,7 +74,7 @@ export function updateLandingPage(version) {

@@ -93,7 +93,7 @@ export function updateLandingPage(version) {

@@ -173,11 +173,11 @@ uiModules.get('apps/management').directive('kbnManagementLanding', function(kbnV FeatureCatalogueRegistryProvider.register(() => { return { - id: 'management', - title: i18n.translate('kbn.management.managementLabel', { - defaultMessage: 'Management', + id: 'stack-management', + title: i18n.translate('kbn.stackManagement.managementLabel', { + defaultMessage: 'Stack Management', }), - description: i18n.translate('kbn.management.managementDescription', { + description: i18n.translate('kbn.stackManagement.managementDescription', { defaultMessage: 'Your center console for managing the Elastic Stack.', }), icon: 'managementApp', diff --git a/src/legacy/core_plugins/kibana/public/management/landing.html b/src/legacy/core_plugins/kibana/public/management/landing.html index a69033e4131c9e..39459b26f74156 100644 --- a/src/legacy/core_plugins/kibana/public/management/landing.html +++ b/src/legacy/core_plugins/kibana/public/management/landing.html @@ -1,3 +1,3 @@ -
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 4b3014fd28a51c..625227be3c2d23 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -83,8 +83,8 @@ - -`; - -describe('fancy forms', function() { - let setup; - const trash = []; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject($injector => { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - setup = function(options = {}) { - const { name = 'person1', tasks = [], onSubmit = () => {} } = options; - - const $el = $(template).appendTo('body'); - trash.push(() => $el.remove()); - const $scope = $rootScope.$new(); - - $scope.name = name; - $scope.tasks = tasks; - $scope.onSubmit = onSubmit; - - $compile($el)($scope); - $scope.$apply(); - - return { - $el, - $scope, - }; - }; - }) - ); - - afterEach(() => trash.splice(0).forEach(fn => fn())); - - describe('nested forms', function() { - it('treats new fields as "soft" errors', function() { - const { $scope } = setup({ name: '' }); - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(0); - }); - - it('upgrades fields to regular errors on attempted submit', function() { - const { $scope, $el } = setup({ name: '' }); - - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(0); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('prevents submit when there are errors', function() { - const onSubmit = sinon.stub(); - const { $scope, $el } = setup({ name: '', onSubmit }); - - expect($scope.person.errorCount()).to.be(1); - sinon.assert.notCalled(onSubmit); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(1); - sinon.assert.notCalled(onSubmit); - - $scope.$apply(() => { - $scope.name = 'foo'; - }); - - expect($scope.person.errorCount()).to.be(0); - sinon.assert.notCalled(onSubmit); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(0); - sinon.assert.calledOnce(onSubmit); - }); - - it('new fields are no longer soft after blur', function() { - const { $scope, $el } = setup({ name: '' }); - expect($scope.person.softErrorCount()).to.be(0); - $el.find(testSubjSelector('name')).blur(); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('counts errors/softErrors in sub forms', function() { - const { $scope, $el } = setup(); - - expect($scope.person.errorCount()).to.be(0); - - $scope.$apply(() => { - $scope.tasks = [ - { - name: 'foo', - description: '', - }, - { - name: 'foo', - description: '', - }, - ]; - }); - - expect($scope.person.errorCount()).to.be(2); - expect($scope.person.softErrorCount()).to.be(0); - - $el - .find(testSubjSelector('taskDesc')) - .first() - .blur(); - - expect($scope.person.errorCount()).to.be(2); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('only counts down', function() { - const { $scope, $el } = setup({ - tasks: [ - { - name: 'foo', - description: '', - }, - { - name: 'bar', - description: '', - }, - { - name: 'baz', - description: '', - }, - ], - }); - - // top level form sees 3 errors - expect($scope.person.errorCount()).to.be(3); - expect($scope.person.softErrorCount()).to.be(0); - - $el - .find('ng-form') - .toArray() - .forEach((el, i) => { - const $task = $(el); - const $taskScope = $task.scope(); - const form = $task.controller('form'); - - // sub forms only see one error - expect(form.errorCount()).to.be(1); - expect(form.softErrorCount()).to.be(0); - - // blurs only count locally - $task.find(testSubjSelector('taskDesc')).blur(); - expect(form.softErrorCount()).to.be(1); - - // but parent form see them - expect($scope.person.softErrorCount()).to.be(1); - - $taskScope.$apply(() => { - $taskScope.task.description = 'valid'; - }); - - expect(form.errorCount()).to.be(0); - expect(form.softErrorCount()).to.be(0); - expect($scope.person.errorCount()).to.be(2 - i); - expect($scope.person.softErrorCount()).to.be(0); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js deleted file mode 100644 index 1f0788cf74d1d0..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiModules } from '../../../../modules'; - -import { decorateFormController } from './kbn_form_controller'; -import { decorateModelController } from './kbn_model_controller'; - -uiModules.get('kibana').config(function($provide) { - $provide.decorator('formDirective', decorateFormController); - $provide.decorator('ngFormDirective', decorateFormController); - $provide.decorator('ngModelDirective', decorateModelController); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js deleted file mode 100644 index 90971140482f7c..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function decorateFormController($delegate, $injector) { - const [directive] = $delegate; - const FormController = directive.controller; - - class KbnFormController extends FormController { - // prevent inheriting FormController's static $inject property - // which is angular's cache of the DI arguments for a function - static $inject = ['$scope', '$element']; - - constructor($scope, $element, ...superArgs) { - super(...superArgs); - - const onSubmit = event => { - this._markInvalidTouched(event); - }; - - $element.on('submit', onSubmit); - $scope.$on('$destroy', () => { - $element.off('submit', onSubmit); - }); - } - - errorCount() { - return this._getInvalidModels().length; - } - - // same as error count, but filters out untouched and pristine models - softErrorCount() { - return this._getInvalidModels().filter(model => model.$touched || model.$dirty).length; - } - - $setTouched() { - this._getInvalidModels().forEach(model => model.$setTouched()); - } - - _markInvalidTouched(event) { - if (this.errorCount()) { - event.preventDefault(); - event.stopImmediatePropagation(); - this.$setTouched(); - } - } - - _getInvalidModels() { - return this.$$controls.reduce((acc, control) => { - // recurse into sub-form - if (typeof control._getInvalidModels === 'function') { - return [...acc, ...control._getInvalidModels()]; - } - - if (control.$invalid) { - return [...acc, control]; - } - - return acc; - }, []); - } - } - - // replace controller with our wrapper - directive.controller = [ - ...$injector.annotate(KbnFormController), - ...$injector.annotate(FormController), - (...args) => new KbnFormController(...args), - ]; - - return $delegate; -} diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js deleted file mode 100644 index bb4d026aa18106..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function decorateModelController($delegate, $injector) { - const [directive] = $delegate; - const ModelController = directive.controller; - - class KbnModelController extends ModelController { - // prevent inheriting ModelController's static $inject property - // which is angular's cache of the DI arguments for a function - static $inject = ['$scope', '$element']; - - constructor($scope, $element, ...superArgs) { - super(...superArgs); - - const onInvalid = () => { - this.$setTouched(); - }; - - // the browser emits an "invalid" event when browser supplied - // validation fails, which implies that the user has indirectly - // interacted with the control and it should be treated as "touched" - $element.on('invalid', onInvalid); - $scope.$on('$destroy', () => { - $element.off('invalid', onInvalid); - }); - } - } - - // replace controller with our wrapper - directive.controller = [ - ...$injector.annotate(KbnModelController), - ...$injector.annotate(ModelController), - (...args) => new KbnModelController(...args), - ]; - - return $delegate; -} diff --git a/src/legacy/ui/public/vis/editors/default/index.ts b/src/legacy/ui/public/vis/editors/default/index.ts index 7079ba23afb5c0..fada4e5d2266ff 100644 --- a/src/legacy/ui/public/vis/editors/default/index.ts +++ b/src/legacy/ui/public/vis/editors/default/index.ts @@ -18,7 +18,7 @@ */ export { AggParamEditorProps } from './components/agg_param_props'; -export { DefaultEditorAggParams, SubAggParamsProp } from './components/agg_params'; +export { DefaultEditorAggParams } from './components/agg_params'; export { ComboBoxGroupedOptions } from './utils'; export * from './vis_options_props'; export * from './utils'; diff --git a/src/legacy/ui/public/vis/editors/default/schemas.ts b/src/legacy/ui/public/vis/editors/default/schemas.ts index e86a73732c3f4c..3cacd1cfbe68f7 100644 --- a/src/legacy/ui/public/vis/editors/default/schemas.ts +++ b/src/legacy/ui/public/vis/editors/default/schemas.ts @@ -28,6 +28,11 @@ import { RadiusRatioOptionControl } from './controls/radius_ratio_option'; import { AggGroupNames } from './agg_groups'; import { AggControlProps } from './controls/agg_control_props'; +export interface ISchemas { + [AggGroupNames.Buckets]: Schema[]; + [AggGroupNames.Metrics]: Schema[]; +} + export interface Schema { aggFilter: string | string[]; editor: boolean | string; diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.html b/src/legacy/ui/public/vis/editors/default/sidebar.html deleted file mode 100644 index b0a03e461fc1ce..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/sidebar.html +++ /dev/null @@ -1,191 +0,0 @@ -
-
- -

- {{ vis.indexPattern.title }} -

- - - -
- - -
- - -
- -
- -
- -
-
diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.js b/src/legacy/ui/public/vis/editors/default/sidebar.js deleted file mode 100644 index 195ae68c0e9599..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/sidebar.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import './agg_group'; -import './vis_options'; -import 'ui/directives/css_truncate'; -import { uiModules } from '../../../modules'; -import sidebarTemplate from './sidebar.html'; -import { move } from '../../../utils/collection'; -import { AggGroupNames } from './agg_groups'; -import { getEnabledMetricAggsCount } from './components/agg_group_helper'; - -uiModules.get('app/visualize').directive('visEditorSidebar', function() { - return { - restrict: 'E', - template: sidebarTemplate, - scope: true, - require: '?^ngModel', - controllerAs: 'sidebar', - controller: function($scope) { - $scope.$watch('vis.type', visType => { - if (visType) { - this.showData = visType.schemas.buckets || visType.schemas.metrics; - if (_.has(visType, 'editorConfig.optionTabs')) { - const activeTabs = visType.editorConfig.optionTabs.filter(tab => { - return _.get(tab, 'active', false); - }); - if (activeTabs.length > 0) { - this.section = activeTabs[0].name; - } - } - this.section = - this.section || - (this.showData ? 'data' : _.get(visType, 'editorConfig.optionTabs[0].name')); - } - }); - - $scope.onAggTypeChange = (agg, value) => { - if (agg.type !== value) { - agg.type = value; - } - }; - - $scope.onAggParamsChange = (params, paramName, value) => { - if (params[paramName] !== value) { - params[paramName] = value; - } - }; - - $scope.addSchema = function(schema) { - const aggConfig = $scope.state.aggs.createAggConfig({ schema }); - aggConfig.brandNew = true; - }; - - $scope.removeAgg = function(agg) { - const aggs = $scope.state.aggs.aggs; - const index = aggs.indexOf(agg); - - if (index === -1) { - return; - } - - aggs.splice(index, 1); - - if (agg.schema.group === AggGroupNames.Metrics) { - const metrics = $scope.state.aggs.bySchemaGroup(AggGroupNames.Metrics); - - if (getEnabledMetricAggsCount(metrics) === 0) { - metrics.find(aggregation => aggregation.schema.name === 'metric').enabled = true; - } - } - }; - - $scope.onToggleEnableAgg = (agg, isEnable) => { - agg.enabled = isEnable; - }; - - $scope.reorderAggs = group => { - //the aggs have been reordered in [group] and we need - //to apply that ordering to [vis.aggs] - const indexOffset = $scope.state.aggs.aggs.indexOf(group[0]); - _.forEach(group, (agg, index) => { - move($scope.state.aggs.aggs, agg, indexOffset + index); - }); - }; - }, - }; -}); diff --git a/src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js b/src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js deleted file mode 100644 index 3cbc94a0293268..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import { uiModules } from '../../../modules'; -import { keyCodes } from '@elastic/eui'; - -uiModules.get('kibana').directive('visEditorResizer', function() { - return { - restrict: 'E', - link: function($scope, $el) { - const $left = $el.parent(); - - $el.on('mousedown', function(event) { - $el.addClass('active'); - const startWidth = $left.width(); - const startX = event.pageX; - - function onMove(event) { - const newWidth = startWidth + event.pageX - startX; - $left.width(newWidth); - } - - $(document.body) - .on('mousemove', onMove) - .one('mouseup', () => { - $el.removeClass('active'); - $(document.body).off('mousemove', onMove); - $scope.$broadcast('render'); - }); - }); - - $el.on('keydown', event => { - const { keyCode } = event; - - if (keyCode === keyCodes.LEFT || keyCode === keyCodes.RIGHT) { - event.preventDefault(); - const startWidth = $left.width(); - const newWidth = startWidth + (keyCode === keyCodes.LEFT ? -15 : 15); - $left.width(newWidth); - $scope.$broadcast('render'); - } - }); - }, - }; -}); diff --git a/src/legacy/ui/public/vis/editors/default/vis_options.js b/src/legacy/ui/public/vis/editors/default/vis_options.js deleted file mode 100644 index 9c9b0353cee270..00000000000000 --- a/src/legacy/ui/public/vis/editors/default/vis_options.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from '../../../modules'; -import { VisOptionsReactWrapper } from './vis_options_react_wrapper'; -import { safeMakeLabel } from './controls/agg_utils'; - -/** - * This directive sort of "transcludes" in whatever template you pass in via the `editor` attribute. - * This lets you specify a full-screen UI for editing a vis type, instead of using the regular - * sidebar. - */ - -uiModules - .get('app/visualize') - .directive('visOptionsReactWrapper', reactDirective => - reactDirective(wrapInI18nContext(VisOptionsReactWrapper), [ - ['component', { wrapApply: false }], - ['aggs', { watchDepth: 'collection' }], - ['stateParams', { watchDepth: 'collection' }], - ['vis', { watchDepth: 'collection' }], - ['uiState', { watchDepth: 'collection' }], - ['setValue', { watchDepth: 'reference' }], - ['setValidity', { watchDepth: 'reference' }], - ['setVisType', { watchDepth: 'reference' }], - ['setTouched', { watchDepth: 'reference' }], - 'hasHistogramAgg', - 'currentTab', - 'aggsLabels', - ]) - ) - .directive('visEditorVisOptions', function($compile) { - return { - restrict: 'E', - require: '?^ngModel', - scope: { - vis: '=', - visData: '=', - uiState: '=', - editor: '=', - visualizeEditor: '=', - editorState: '=', - onAggParamsChange: '=', - hasHistogramAgg: '=', - currentTab: '=', - }, - link: function($scope, $el, attrs, ngModelCtrl) { - $scope.setValue = (paramName, value) => - $scope.onAggParamsChange($scope.editorState.params, paramName, value); - - $scope.setValidity = isValid => { - ngModelCtrl.$setValidity(`visOptions`, isValid); - }; - - $scope.setTouched = isTouched => { - if (isTouched) { - ngModelCtrl.$setTouched(); - } else { - ngModelCtrl.$setUntouched(); - } - }; - - $scope.setVisType = type => { - $scope.vis.type.type = type; - }; - - // since aggs reference isn't changed when an agg is updated, we need somehow to let React component know about it - $scope.aggsLabels = ''; - - $scope.$watch( - () => { - return $scope.editorState.aggs.aggs - .map(agg => { - return safeMakeLabel(agg); - }) - .join(); - }, - value => { - $scope.aggsLabels = value; - } - ); - - const comp = - typeof $scope.editor === 'string' - ? $scope.editor - : ` - `; - const $editor = $compile(comp)($scope); - $el.append($editor); - }, - }; - }); diff --git a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx index 3f9fa9f5f352f4..5b4badc1036457 100644 --- a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx +++ b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx @@ -23,13 +23,12 @@ import { Vis } from './../..'; export interface VisOptionsProps { aggs: AggConfigs; - aggsLabels: string; hasHistogramAgg: boolean; + isTabSelected: boolean; stateParams: VisParamType; vis: Vis; uiState: PersistedState; setValue(paramName: T, value: VisParamType[T]): void; setValidity(isValid: boolean): void; - setVisType(type: string): void; setTouched(isTouched: boolean): void; } diff --git a/src/legacy/ui/public/vis/vis_types/_index.scss b/src/legacy/ui/public/vis/vis_types/_index.scss deleted file mode 100644 index 9d86383ec40b2e..00000000000000 --- a/src/legacy/ui/public/vis/vis_types/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './vislib_vis_type'; -@import './vislib_vis_legend'; diff --git a/src/legacy/ui/public/vis/vis_types/angular_vis_type.js b/src/legacy/ui/public/vis/vis_types/angular_vis_type.js index 88412c76d0d366..c34294d45548c5 100644 --- a/src/legacy/ui/public/vis/vis_types/angular_vis_type.js +++ b/src/legacy/ui/public/vis/vis_types/angular_vis_type.js @@ -18,6 +18,7 @@ */ import $ from 'jquery'; +import { isEqual } from 'lodash'; import chrome from 'ui/chrome'; export class AngularVisController { @@ -37,6 +38,11 @@ export class AngularVisController { this.$scope.vis = this.vis; this.$scope.visState = this.vis.getState(); this.$scope.esResponse = esResponse; + + if (!isEqual(this.$scope.visParams, visParams)) { + this.vis.emit('updateEditorStateParams', visParams); + } + this.$scope.visParams = visParams; this.$scope.renderComplete = resolve; this.$scope.renderFailed = reject; diff --git a/src/legacy/ui/ui_apps/ui_app.js b/src/legacy/ui/ui_apps/ui_app.js index 9c82ff2abedb57..1cfd54588b516e 100644 --- a/src/legacy/ui/ui_apps/ui_app.js +++ b/src/legacy/ui/ui_apps/ui_app.js @@ -32,6 +32,7 @@ export class UiApp { hidden, linkToLastSubUrl, listed, + category, url = `/app/${id}`, } = spec; @@ -46,6 +47,7 @@ export class UiApp { this._icon = icon; this._euiIconType = euiIconType; this._linkToLastSubUrl = linkToLastSubUrl; + this._category = category; this._hidden = hidden; this._listed = listed; this._url = url; @@ -68,6 +70,7 @@ export class UiApp { euiIconType: this._euiIconType, url: this._url, linkToLastSubUrl: this._linkToLastSubUrl, + category: this._category, }); } } @@ -115,6 +118,7 @@ export class UiApp { main: this._main, navLink: this._navLink, linkToLastSubUrl: this._linkToLastSubUrl, + category: this._category, }; } } diff --git a/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js b/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js index d7ac49d9d49a32..639a5a7c58e180 100644 --- a/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js +++ b/src/legacy/ui/ui_exports/ui_export_types/ui_apps.js @@ -34,6 +34,7 @@ function applySpecDefaults(spec, type, pluginSpec) { linkToLastSubUrl = true, listed = !hidden, url = `/app/${id}`, + category, } = spec; if (spec.injectVars) { @@ -61,6 +62,7 @@ function applySpecDefaults(spec, type, pluginSpec) { linkToLastSubUrl, listed, url, + category, }; } diff --git a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js b/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js index 37e023127ed419..543fe05b13e43e 100644 --- a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js +++ b/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js @@ -45,6 +45,7 @@ describe('UiNavLink', () => { euiIconType: spec.euiIconType, hidden: spec.hidden, disabled: spec.disabled, + category: undefined, // defaults linkToLastSubUrl: true, diff --git a/src/legacy/ui/ui_nav_links/ui_nav_link.js b/src/legacy/ui/ui_nav_links/ui_nav_link.js index 7537a60adbcf2d..5888c21a53c95c 100644 --- a/src/legacy/ui/ui_nav_links/ui_nav_link.js +++ b/src/legacy/ui/ui_nav_links/ui_nav_link.js @@ -31,6 +31,7 @@ export class UiNavLink { hidden = false, disabled = false, tooltip = '', + category, } = spec; this._id = id; @@ -44,6 +45,7 @@ export class UiNavLink { this._hidden = hidden; this._disabled = disabled; this._tooltip = tooltip; + this._category = category; } getOrder() { @@ -63,6 +65,7 @@ export class UiNavLink { hidden: this._hidden, disabled: this._disabled, tooltip: this._tooltip, + category: this._category, }; } } diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts new file mode 100644 index 00000000000000..0527f833b0f8c3 --- /dev/null +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup } from 'src/core/public'; +import { QuerySuggestionsGetFn } from './providers/query_suggestion_provider'; +import { + setupValueSuggestionProvider, + ValueSuggestionsGetFn, +} from './providers/value_suggestion_provider'; + +export class AutocompleteService { + private readonly querySuggestionProviders: Map = new Map(); + private getValueSuggestions?: ValueSuggestionsGetFn; + + private addQuerySuggestionProvider = ( + language: string, + provider: QuerySuggestionsGetFn + ): void => { + if (language && provider) { + this.querySuggestionProviders.set(language, provider); + } + }; + + private getQuerySuggestions: QuerySuggestionsGetFn = args => { + const { language } = args; + const provider = this.querySuggestionProviders.get(language); + + if (provider) { + return provider(args); + } + }; + + private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language); + + /** @public **/ + public setup(core: CoreSetup) { + this.getValueSuggestions = setupValueSuggestionProvider(core); + + return { + addQuerySuggestionProvider: this.addQuerySuggestionProvider, + + /** @obsolete **/ + /** please use "getProvider" only from the start contract **/ + getQuerySuggestions: this.getQuerySuggestions, + }; + } + + /** @public **/ + public start() { + return { + getQuerySuggestions: this.getQuerySuggestions, + hasQuerySuggestions: this.hasQuerySuggestions, + getValueSuggestions: this.getValueSuggestions!, + }; + } + + /** @internal **/ + public clearProviders(): void { + this.querySuggestionProviders.clear(); + } +} diff --git a/src/plugins/data/public/autocomplete/index.ts b/src/plugins/data/public/autocomplete/index.ts new file mode 100644 index 00000000000000..5b8f3ae510bfd5 --- /dev/null +++ b/src/plugins/data/public/autocomplete/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { AutocompleteService } from './autocomplete_service'; +export { QuerySuggestion, QuerySuggestionType, QuerySuggestionsGetFn } from './types'; diff --git a/src/plugins/data/public/autocomplete_provider/types.ts b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts similarity index 59% rename from src/plugins/data/public/autocomplete_provider/types.ts rename to src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts index 389057f94144d2..53abdd44c0c3f3 100644 --- a/src/plugins/data/public/autocomplete_provider/types.ts +++ b/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts @@ -17,56 +17,40 @@ * under the License. */ -import { AutocompleteProviderRegister } from '.'; -import { IIndexPattern, IFieldType } from '../../common'; +import { IFieldType, IIndexPattern } from '../../../common/index_patterns'; -export type AutocompletePublicPluginSetup = Pick< - AutocompleteProviderRegister, - 'addProvider' | 'getProvider' ->; -export type AutocompletePublicPluginStart = Pick; +export type QuerySuggestionType = 'field' | 'value' | 'operator' | 'conjunction' | 'recentSearch'; -/** @public **/ -export type AutocompleteProvider = (args: { - config: { - get(configKey: string): any; - }; - indexPatterns: IIndexPattern[]; - boolFilter?: any; -}) => GetSuggestions; +export type QuerySuggestionsGetFn = ( + args: QuerySuggestionsGetFnArgs +) => Promise | undefined; -/** @public **/ -export type GetSuggestions = (args: { +interface QuerySuggestionsGetFnArgs { + language: string; + indexPatterns: IIndexPattern[]; query: string; selectionStart: number; selectionEnd: number; signal?: AbortSignal; -}) => Promise; - -/** @public **/ -export type AutocompleteSuggestionType = - | 'field' - | 'value' - | 'operator' - | 'conjunction' - | 'recentSearch'; - -// A union type allows us to do easy type guards in the code. For example, if I want to ensure I'm -// working with a FieldAutocompleteSuggestion, I can just do `if ('field' in suggestion)` and the -// TypeScript compiler will narrow the type to the parts of the union that have a field prop. -/** @public **/ -export type AutocompleteSuggestion = BasicAutocompleteSuggestion | FieldAutocompleteSuggestion; + boolFilter?: any; +} -interface BasicAutocompleteSuggestion { +interface BasicQuerySuggestion { + type: QuerySuggestionType; description?: string; end: number; start: number; text: string; - type: AutocompleteSuggestionType; cursorIndex?: number; } -export type FieldAutocompleteSuggestion = BasicAutocompleteSuggestion & { +interface FieldQuerySuggestion extends BasicQuerySuggestion { type: 'field'; field: IFieldType; -}; +} + +// A union type allows us to do easy type guards in the code. For example, if I want to ensure I'm +// working with a FieldAutocompleteSuggestion, I can just do `if ('field' in suggestion)` and the +// TypeScript compiler will narrow the type to the parts of the union that have a field prop. +/** @public **/ +export type QuerySuggestion = BasicQuerySuggestion | FieldQuerySuggestion; diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts similarity index 51% rename from src/plugins/data/public/suggestions_provider/value_suggestions.test.ts rename to src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 9089105b4e3a82..6b0c0f07cf6c95 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -17,98 +17,121 @@ * under the License. */ -import { stubIndexPattern, stubFields } from '../stubs'; -import { getSuggestionsProvider } from './value_suggestions'; -import { IUiSettingsClient } from 'kibana/public'; +import { stubIndexPattern, stubFields } from '../../stubs'; +import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider'; +import { IUiSettingsClient, CoreSetup } from 'kibana/public'; -describe('getSuggestions', () => { - let getSuggestions: any; +describe('FieldSuggestions', () => { + let getValueSuggestions: ValueSuggestionsGetFn; let http: any; + let shouldSuggestValues: boolean; - describe('with value suggestions disabled', () => { - beforeEach(() => { - const config = { get: (key: string) => false } as IUiSettingsClient; - http = { fetch: jest.fn() }; - getSuggestions = getSuggestionsProvider(config, http); - }); + beforeEach(() => { + const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient; + http = { fetch: jest.fn() }; + getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup); + }); + + describe('with value suggestions disabled', () => { it('should return an empty array', async () => { - const index = stubIndexPattern.id; - const [field] = stubFields; - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: stubFields[0], + query: '', + }); + expect(suggestions).toEqual([]); expect(http.fetch).not.toHaveBeenCalled(); }); }); describe('with value suggestions enabled', () => { - beforeEach(() => { - const config = { get: (key: string) => true } as IUiSettingsClient; - http = { fetch: jest.fn() }; - getSuggestions = getSuggestionsProvider(config, http); - }); + shouldSuggestValues = true; it('should return true/false for boolean fields', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter(({ type }) => type === 'boolean'); - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([true, false]); expect(http.fetch).not.toHaveBeenCalled(); }); it('should return an empty array if the field type is not a string or boolean', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter(({ type }) => type !== 'string' && type !== 'boolean'); - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([]); expect(http.fetch).not.toHaveBeenCalled(); }); it('should return an empty array if the field is not aggregatable', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter(({ aggregatable }) => !aggregatable); - const query = ''; - const suggestions = await getSuggestions(index, field, query); + const suggestions = await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(suggestions).toEqual([]); expect(http.fetch).not.toHaveBeenCalled(); }); it('should otherwise request suggestions', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - const query = ''; - await getSuggestions(index, field, query); + + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field, + query: '', + }); + expect(http.fetch).toHaveBeenCalled(); }); it('should cache results if using the same index/field/query/filter', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - const query = ''; - await getSuggestions(index, field, query); - await getSuggestions(index, field, query); + const args = { + indexPattern: stubIndexPattern, + field, + query: '', + }; + + await getValueSuggestions(args); + await getValueSuggestions(args); + expect(http.fetch).toHaveBeenCalledTimes(1); }); it('should cache results for only one minute', async () => { - const index = stubIndexPattern.id; const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - const query = ''; + const args = { + indexPattern: stubIndexPattern, + field, + query: '', + }; const { now } = Date; Date.now = jest.fn(() => 0); - await getSuggestions(index, field, query); + + await getValueSuggestions(args); + Date.now = jest.fn(() => 60 * 1000); - await getSuggestions(index, field, query); + await getValueSuggestions(args); Date.now = now; expect(http.fetch).toHaveBeenCalledTimes(2); @@ -118,14 +141,54 @@ describe('getSuggestions', () => { const fields = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); - await getSuggestions('index', fields[0], ''); - await getSuggestions('index', fields[0], 'query'); - await getSuggestions('index', fields[1], ''); - await getSuggestions('index', fields[1], 'query'); - await getSuggestions('logstash-*', fields[0], ''); - await getSuggestions('logstash-*', fields[0], 'query'); - await getSuggestions('logstash-*', fields[1], ''); - await getSuggestions('logstash-*', fields[1], 'query'); + + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[0], + query: '', + }); + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[0], + query: 'query', + }); + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[1], + query: '', + }); + await getValueSuggestions({ + indexPattern: stubIndexPattern, + field: fields[1], + query: 'query', + }); + + const customIndexPattern = { + ...stubIndexPattern, + title: 'customIndexPattern', + }; + + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[0], + query: '', + }); + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[0], + query: 'query', + }); + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[1], + query: '', + }); + await getValueSuggestions({ + indexPattern: customIndexPattern, + field: fields[1], + query: 'query', + }); + expect(http.fetch).toHaveBeenCalledTimes(8); }); }); diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts similarity index 55% rename from src/plugins/data/public/suggestions_provider/value_suggestions.ts rename to src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index e64156c290db10..5df88000edbd5f 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -18,51 +18,53 @@ */ import { memoize } from 'lodash'; +import { CoreSetup } from 'src/core/public'; +import { IIndexPattern, IFieldType } from '../../../common'; -import { IUiSettingsClient, HttpSetup } from 'src/core/public'; -import { IGetSuggestions } from './types'; -import { IFieldType } from '../../common'; +function resolver(title: string, field: IFieldType, query: string, boolFilter: any) { + // Only cache results for a minute + const ttl = Math.floor(Date.now() / 1000 / 60); + + return [ttl, query, title, field.name, JSON.stringify(boolFilter)].join('|'); +} + +export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; + +interface ValueSuggestionsGetFnArgs { + indexPattern: IIndexPattern; + field: IFieldType; + query: string; + boolFilter?: any[]; + signal?: AbortSignal; +} -export function getSuggestionsProvider( - uiSettings: IUiSettingsClient, - http: HttpSetup -): IGetSuggestions { +export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => { const requestSuggestions = memoize( - ( - index: string, - field: IFieldType, - query: string, - boolFilter: any = [], - signal?: AbortSignal - ) => { - return http.fetch(`/api/kibana/suggestions/values/${index}`, { + (index: string, field: IFieldType, query: string, boolFilter: any = [], signal?: AbortSignal) => + core.http.fetch(`/api/kibana/suggestions/values/${index}`, { method: 'POST', body: JSON.stringify({ query, field: field.name, boolFilter }), signal, - }); - }, + }), resolver ); - return async ( - index: string, - field: IFieldType, - query: string, - boolFilter?: any, - signal?: AbortSignal - ) => { - const shouldSuggestValues = uiSettings.get('filterEditor:suggestValues'); + return async ({ + indexPattern, + field, + query, + boolFilter, + signal, + }: ValueSuggestionsGetFnArgs): Promise => { + const shouldSuggestValues = core!.uiSettings.get('filterEditor:suggestValues'); + const { title } = indexPattern; + if (field.type === 'boolean') { return [true, false]; } else if (!shouldSuggestValues || !field.aggregatable || field.type !== 'string') { return []; } - return await requestSuggestions(index, field, query, boolFilter, signal); - }; -} -function resolver(index: string, field: IFieldType, query: string, boolFilter: any) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, index, field.name, JSON.stringify(boolFilter)].join('|'); -} + return await requestSuggestions(title, field, query, boolFilter, signal); + }; +}; diff --git a/src/plugins/data/public/autocomplete/types.ts b/src/plugins/data/public/autocomplete/types.ts new file mode 100644 index 00000000000000..759e2dd25a5bc1 --- /dev/null +++ b/src/plugins/data/public/autocomplete/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { AutocompleteService } from './autocomplete_service'; + +/** @public **/ +export type AutocompleteSetup = ReturnType; + +/** @public **/ +export type AutocompleteStart = ReturnType; + +/** @public **/ +export { + QuerySuggestion, + QuerySuggestionsGetFn, + QuerySuggestionType, +} from './providers/query_suggestion_provider'; diff --git a/src/plugins/data/public/autocomplete_provider/index.ts b/src/plugins/data/public/autocomplete_provider/index.ts deleted file mode 100644 index 6758bd7f379c12..00000000000000 --- a/src/plugins/data/public/autocomplete_provider/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { AutocompleteProvider } from './types'; - -export class AutocompleteProviderRegister { - private readonly registeredProviders: Map = new Map(); - - /** @public **/ - public addProvider(language: string, provider: AutocompleteProvider): void { - if (language && provider) { - this.registeredProviders.set(language, provider); - } - } - - /** @public **/ - public getProvider(language: string): AutocompleteProvider | undefined { - return this.registeredProviders.get(language); - } - - /** @internal **/ - public clearProviders(): void { - this.registeredProviders.clear(); - } -} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 4b330600417e7d..19ba246ce02dd2 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -18,6 +18,8 @@ */ import { PluginInitializerContext } from '../../../core/public'; +import * as autocomplete from './autocomplete'; + export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); } @@ -49,11 +51,6 @@ export { TimeRange, } from '../common'; -/** - * Static code to be shared externally - * @public - */ -export * from './autocomplete_provider'; export * from './field_formats_provider'; export * from './index_patterns'; export * from './search'; @@ -97,3 +94,5 @@ export { // Export plugin after all other imports import { DataPublicPlugin } from './plugin'; export { DataPublicPlugin as Plugin }; + +export { autocomplete }; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 03d3dad61ed052..f44d40f533eed3 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -30,9 +30,9 @@ export type Setup = jest.Mocked>; export type Start = jest.Mocked>; const autocompleteMock: any = { - addProvider: jest.fn(), - getProvider: jest.fn(), - clearProviders: jest.fn(), + getValueSuggestions: jest.fn(), + getQuerySuggestions: jest.fn(), + hasQuerySuggestions: jest.fn(), }; const fieldFormatsMock: PublicMethodsOf = { @@ -69,13 +69,28 @@ const createStartContract = (): Start => { const startContract = { autocomplete: autocompleteMock, getSuggestions: jest.fn(), - search: { search: jest.fn() }, + search: { + search: jest.fn(), + + __LEGACY: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, + }, fieldFormats: fieldFormatsMock as FieldFormatsStart, query: queryStartMock, ui: { IndexPatternSelect: jest.fn(), SearchBar: jest.fn(), }, + __LEGACY: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, indexPatterns: {} as IndexPatternsContract, }; return startContract; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index cd55048ca527fa..ce8ca0317bd7d8 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -17,7 +17,13 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + PackageInfo, +} from 'src/core/public'; import { Storage, IStorageWrapper } from '../../kibana_utils/public'; import { DataPublicPluginSetup, @@ -25,30 +31,37 @@ import { DataSetupDependencies, DataStartDependencies, } from './types'; -import { AutocompleteProviderRegister } from './autocomplete_provider'; -import { getSuggestionsProvider } from './suggestions_provider'; +import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats_provider'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; import { IndexPatterns } from './index_patterns'; -import { setNotifications, setFieldFormats, setOverlays, setIndexPatterns } from './services'; +import { + setNotifications, + setFieldFormats, + setOverlays, + setIndexPatterns, + setHttp, +} from './services'; import { createFilterAction, GLOBAL_APPLY_FILTER_ACTION } from './actions'; import { APPLY_FILTER_TRIGGER } from '../../embeddable/public'; import { createSearchBar } from './ui/search_bar/create_search_bar'; export class DataPublicPlugin implements Plugin { - private readonly autocomplete = new AutocompleteProviderRegister(); + private readonly autocomplete = new AutocompleteService(); private readonly searchService: SearchService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; + private readonly packageInfo: PackageInfo; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); this.storage = new Storage(window.localStorage); + this.packageInfo = initializerContext.env.packageInfo; } public setup(core: CoreSetup, { uiActions }: DataSetupDependencies): DataPublicPluginSetup { @@ -62,7 +75,7 @@ export class DataPublicPlugin implements Plugin { - if (a!.store === b!.store) { - return 0; - } else { - return a!.store === esFilters.FilterStateStore.GLOBAL_STATE && - b!.store !== esFilters.FilterStateStore.GLOBAL_STATE - ? -1 - : 1; - } - }); + newFilters.sort(sortFilters); - const filtersUpdated = !_.isEqual(this.filters, newFilters); + const filtersUpdated = !compareFilters(this.filters, newFilters, COMPARE_ALL_OPTIONS); const updatedOnlyDisabledFilters = onlyDisabledFiltersChanged(newFilters, this.filters); this.filters = newFilters; diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts index 34fd662c4ba460..9cc5938750c4ed 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { compareFilters } from './compare_filters'; +import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; import { esFilters } from '../../../../common'; describe('filter manager utilities', () => { @@ -83,5 +83,134 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); + + test('should compare filters array to non array', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'mochi', type: 'phrase' } } }, + 'index', + '' + ); + + expect(compareFilters([f1, f2], f1)).toBeFalsy(); + }); + + test('should compare filters array to partial array', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'mochi', type: 'phrase' } } }, + 'index', + '' + ); + + expect(compareFilters([f1, f2], [f1])).toBeFalsy(); + }); + + test('should compare filters array to exact array', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'mochi', type: 'phrase' } } }, + 'index', + '' + ); + + expect(compareFilters([f1, f2], [f1, f2])).toBeTruthy(); + }); + + test('should compare array of duplicates, ignoring meta attributes', () => { + const f1 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index1', + '' + ); + const f2 = esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index2', + '' + ); + + expect(compareFilters([f1], [f2])).toBeTruthy(); + }); + + test('should compare array of duplicates, ignoring $state attributes', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + expect(compareFilters([f1], [f2])).toBeTruthy(); + }); + + test('should compare duplicates with COMPARE_ALL_OPTIONS should check store', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeFalsy(); + }); + + test('should compare duplicates with COMPARE_ALL_OPTIONS should not check key and value ', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + f2.meta.key = 'wassup'; + f2.meta.value = 'dog'; + + expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeTruthy(); + }); }); }); diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts index 9b171ab0aacb2a..218b9d492b61f6 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts @@ -17,32 +17,64 @@ * under the License. */ -import { defaults, isEqual, omit } from 'lodash'; +import { defaults, isEqual, omit, map } from 'lodash'; import { esFilters } from '../../../../common'; +export interface FilterCompareOptions { + disabled?: boolean; + negate?: boolean; + state?: boolean; +} + +/** + * Include disabled, negate and store when comparing filters + */ +export const COMPARE_ALL_OPTIONS: FilterCompareOptions = { + disabled: true, + negate: true, + state: true, +}; + +const mapFilter = ( + filter: esFilters.Filter, + comparators: FilterCompareOptions, + excludedAttributes: string[] +) => { + const cleaned: esFilters.FilterMeta = omit(filter, excludedAttributes); + + if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); + if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); + + return cleaned; +}; + +const mapFilterArray = ( + filters: esFilters.Filter[], + comparators: FilterCompareOptions, + excludedAttributes: string[] +) => { + return map(filters, (filter: esFilters.Filter) => + mapFilter(filter, comparators, excludedAttributes) + ); +}; + /** - * Compare two filters to see if they match + * Compare two filters or filter arrays to see if they match. + * For filter arrays, the assumption is they are sorted. * - * @param {object} first The first filter to compare - * @param {object} second The second filter to compare - * @param {object} comparatorOptions Parameters to use for comparison + * @param {esFilters.Filter | esFilters.Filter[]} first The first filter or filter array to compare + * @param {esFilters.Filter | esFilters.Filter[]} second The second filter or filter array to compare + * @param {FilterCompareOptions} comparatorOptions Parameters to use for comparison * * @returns {bool} Filters are the same */ export const compareFilters = ( - first: esFilters.Filter, - second: esFilters.Filter, - comparatorOptions: any = {} + first: esFilters.Filter | esFilters.Filter[], + second: esFilters.Filter | esFilters.Filter[], + comparatorOptions: FilterCompareOptions = {} ) => { - let comparators: any = {}; - const mapFilter = (filter: esFilters.Filter) => { - const cleaned: esFilters.FilterMeta = omit(filter, excludedAttributes); - - if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); - if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); + let comparators: FilterCompareOptions = {}; - return cleaned; - }; const excludedAttributes: string[] = ['$$hashKey', 'meta']; comparators = defaults(comparatorOptions || {}, { @@ -53,5 +85,17 @@ export const compareFilters = ( if (!comparators.state) excludedAttributes.push('$state'); - return isEqual(mapFilter(first), mapFilter(second)); + if (Array.isArray(first) && Array.isArray(second)) { + return isEqual( + mapFilterArray(first, comparators, excludedAttributes), + mapFilterArray(second, comparators, excludedAttributes) + ); + } else if (!Array.isArray(first) && !Array.isArray(second)) { + return isEqual( + mapFilter(first, comparators, excludedAttributes), + mapFilter(second, comparators, excludedAttributes) + ); + } else { + return false; + } }; diff --git a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts b/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts index 6dae14f480b4f1..897a645e87b5a7 100644 --- a/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/dedup_filters.ts @@ -18,7 +18,7 @@ */ import { filter, find } from 'lodash'; -import { compareFilters } from './compare_filters'; +import { compareFilters, FilterCompareOptions } from './compare_filters'; import { esFilters } from '../../../../common'; /** @@ -33,7 +33,7 @@ import { esFilters } from '../../../../common'; export const dedupFilters = ( existingFilters: esFilters.Filter[], filters: esFilters.Filter[], - comparatorOptions: any = {} + comparatorOptions: FilterCompareOptions = {} ) => { if (!Array.isArray(filters)) { filters = [filters]; diff --git a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts index c040d2f2960c75..3f35c94a3f858f 100644 --- a/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts +++ b/src/plugins/data/public/query/filter_manager/lib/only_disabled.ts @@ -17,8 +17,9 @@ * under the License. */ -import { filter, isEqual } from 'lodash'; +import { filter } from 'lodash'; import { esFilters } from '../../../../common'; +import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; const isEnabled = (f: esFilters.Filter) => f && f.meta && !f.meta.disabled; @@ -35,5 +36,5 @@ export const onlyDisabledFiltersChanged = ( const newEnabledFilters = filter(newFilters || [], isEnabled); const oldEnabledFilters = filter(oldFilters || [], isEnabled); - return isEqual(oldEnabledFilters, newEnabledFilters); + return compareFilters(oldEnabledFilters, newEnabledFilters, COMPARE_ALL_OPTIONS); }; diff --git a/src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts new file mode 100644 index 00000000000000..949c57e43ce743 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/sort_filters.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { sortFilters } from './sort_filters'; +import { esFilters } from '../../../../common'; + +describe('sortFilters', () => { + describe('sortFilters()', () => { + test('Not sort two application level filters', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + const filters = [f1, f2].sort(sortFilters); + expect(filters[0]).toBe(f1); + }); + + test('Not sort two global level filters', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + const filters = [f1, f2].sort(sortFilters); + expect(filters[0]).toBe(f1); + }); + + test('Move global level filter to the beginning of the array', () => { + const f1 = { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + const f2 = { + $state: { store: esFilters.FilterStateStore.GLOBAL_STATE }, + ...esFilters.buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ), + }; + + const filters = [f1, f2].sort(sortFilters); + expect(filters[0]).toBe(f2); + }); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/sort_filters.ts b/src/plugins/data/public/query/filter_manager/lib/sort_filters.ts new file mode 100644 index 00000000000000..657c80fe0ccb23 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/sort_filters.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { esFilters } from '../../../../common'; + +/** + * Sort filters according to their store - global filters go first + * + * @param {object} first The first filter to compare + * @param {object} second The second filter to compare + * + * @returns {number} Sorting order of filters + */ +export const sortFilters = ( + { $state: a }: esFilters.Filter, + { $state: b }: esFilters.Filter +): number => { + if (a!.store === b!.store) { + return 0; + } else { + return a!.store === esFilters.FilterStateStore.GLOBAL_STATE && + b!.store !== esFilters.FilterStateStore.GLOBAL_STATE + ? -1 + : 1; + } +}; diff --git a/src/plugins/data/public/search/create_app_mount_context_search.test.ts b/src/plugins/data/public/search/create_app_mount_context_search.test.ts index fa7cdbcda3082b..15b85ee270bed1 100644 --- a/src/plugins/data/public/search/create_app_mount_context_search.test.ts +++ b/src/plugins/data/public/search/create_app_mount_context_search.test.ts @@ -18,32 +18,35 @@ */ import { createAppMountSearchContext } from './create_app_mount_context_search'; -import { from } from 'rxjs'; +import { from, BehaviorSubject } from 'rxjs'; describe('Create app mount search context', () => { it('Returns search fn when there are no strategies', () => { - const context = createAppMountSearchContext({}); + const context = createAppMountSearchContext({}, new BehaviorSubject(0)); expect(context.search).toBeDefined(); }); it(`Search throws an error when the strategy doesn't exist`, () => { - const context = createAppMountSearchContext({}); + const context = createAppMountSearchContext({}, new BehaviorSubject(0)); expect(() => context.search({}, {}, 'noexist').toPromise()).toThrowErrorMatchingInlineSnapshot( `"Strategy with name noexist does not exist"` ); }); it(`Search fn is called on appropriate strategy name`, done => { - const context = createAppMountSearchContext({ - mysearch: search => - Promise.resolve({ - search: () => from(Promise.resolve({ percentComplete: 98 })), - }), - anothersearch: search => - Promise.resolve({ - search: () => from(Promise.resolve({ percentComplete: 0 })), - }), - }); + const context = createAppMountSearchContext( + { + mysearch: search => + Promise.resolve({ + search: () => from(Promise.resolve({ percentComplete: 98 })), + }), + anothersearch: search => + Promise.resolve({ + search: () => from(Promise.resolve({ percentComplete: 0 })), + }), + }, + new BehaviorSubject(0) + ); context.search({}, {}, 'mysearch').subscribe(response => { expect(response).toEqual({ percentComplete: 98 }); @@ -52,16 +55,19 @@ describe('Create app mount search context', () => { }); it(`Search fn is called with the passed in request object`, done => { - const context = createAppMountSearchContext({ - mysearch: search => { - return Promise.resolve({ - search: request => { - expect(request).toEqual({ greeting: 'hi' }); - return from(Promise.resolve({})); - }, - }); + const context = createAppMountSearchContext( + { + mysearch: search => { + return Promise.resolve({ + search: request => { + expect(request).toEqual({ greeting: 'hi' }); + return from(Promise.resolve({})); + }, + }); + }, }, - }); + new BehaviorSubject(0) + ); context.search({ greeting: 'hi' } as any, {}, 'mysearch').subscribe( response => {}, () => {}, diff --git a/src/plugins/data/public/search/create_app_mount_context_search.ts b/src/plugins/data/public/search/create_app_mount_context_search.ts index 5659a9c863dc18..f480b8f3e042e6 100644 --- a/src/plugins/data/public/search/create_app_mount_context_search.ts +++ b/src/plugins/data/public/search/create_app_mount_context_search.ts @@ -17,8 +17,8 @@ * under the License. */ -import { mergeMap } from 'rxjs/operators'; -import { from } from 'rxjs'; +import { mergeMap, tap } from 'rxjs/operators'; +import { from, BehaviorSubject } from 'rxjs'; import { ISearchAppMountContext } from './i_search_app_mount_context'; import { ISearchGeneric } from './i_search'; import { @@ -30,7 +30,8 @@ import { TStrategyTypes } from './strategy_types'; import { DEFAULT_SEARCH_STRATEGY } from '../../common/search'; export const createAppMountSearchContext = ( - searchStrategies: TSearchStrategiesMap + searchStrategies: TSearchStrategiesMap, + loadingCount$: BehaviorSubject ): ISearchAppMountContext => { const getSearchStrategy = ( strategyName?: K @@ -48,7 +49,13 @@ export const createAppMountSearchContext = ( const strategyPromise = getSearchStrategy(strategyName); return from(strategyPromise).pipe( mergeMap(strategy => { - return strategy.search(request, options); + loadingCount$.next(loadingCount$.getValue() + 1); + return strategy.search(request, options).pipe( + tap( + error => loadingCount$.next(loadingCount$.getValue() - 1), + complete => loadingCount$.next(loadingCount$.getValue() - 1) + ) + ); }) ); }; diff --git a/src/plugins/data/public/search/es_client/get_es_client.ts b/src/plugins/data/public/search/es_client/get_es_client.ts new file mode 100644 index 00000000000000..6c271643ba0124 --- /dev/null +++ b/src/plugins/data/public/search/es_client/get_es_client.ts @@ -0,0 +1,93 @@ +/* + * 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. + */ + +// @ts-ignore +import { default as es } from 'elasticsearch-browser/elasticsearch'; +import { CoreStart, PackageInfo } from 'kibana/public'; +import { BehaviorSubject } from 'rxjs'; + +export function getEsClient( + injectedMetadata: CoreStart['injectedMetadata'], + http: CoreStart['http'], + packageInfo: PackageInfo, + loadingCount$: BehaviorSubject +) { + const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number; + const esApiVersion = injectedMetadata.getInjectedVar('esApiVersion') as string; + + // Use legacy es client for msearch. + const client = es.Client({ + host: getEsUrl(http, packageInfo), + log: 'info', + requestTimeout: esRequestTimeout, + apiVersion: esApiVersion, + }); + + return { + search: wrapEsClientMethod(client, 'search', loadingCount$), + msearch: wrapEsClientMethod(client, 'msearch', loadingCount$), + create: wrapEsClientMethod(client, 'create', loadingCount$), + }; +} + +function wrapEsClientMethod(esClient: any, method: string, loadingCount$: BehaviorSubject) { + return (args: any) => { + // esClient returns a promise, with an additional abort handler + // To tap into the abort handling, we have to override that abort handler. + const customPromiseThingy = esClient[method](args); + const { abort } = customPromiseThingy; + let resolved = false; + + // Start LoadingIndicator + loadingCount$.next(loadingCount$.getValue() + 1); + + // Stop LoadingIndicator when user aborts + customPromiseThingy.abort = () => { + abort(); + if (!resolved) { + resolved = true; + loadingCount$.next(loadingCount$.getValue() - 1); + } + }; + + // Stop LoadingIndicator when promise finishes + customPromiseThingy.finally(() => { + resolved = true; + loadingCount$.next(loadingCount$.getValue() - 1); + }); + + return customPromiseThingy; + }; +} + +function getEsUrl(http: CoreStart['http'], packageInfo: PackageInfo) { + const a = document.createElement('a'); + a.href = http.basePath.prepend('/elasticsearch'); + const protocolPort = /https/.test(a.protocol) ? 443 : 80; + const port = a.port || protocolPort; + return { + host: a.hostname, + port, + protocol: a.protocol, + pathname: a.pathname, + headers: { + 'kbn-version': packageInfo.version, + }, + }; +} diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js b/src/plugins/data/public/search/es_client/index.ts similarity index 94% rename from src/legacy/ui/public/vis/editors/default/fancy_forms/index.js rename to src/plugins/data/public/search/es_client/index.ts index 927e6d69e3c8a3..bf1a3f5d6e7c4f 100644 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js +++ b/src/plugins/data/public/search/es_client/index.ts @@ -17,4 +17,4 @@ * under the License. */ -import './fancy_forms'; +export { getEsClient } from './get_es_client'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6030884c9f6b1d..6f3e228939d6dd 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { BehaviorSubject } from 'rxjs'; import { Plugin, CoreSetup, @@ -23,6 +24,7 @@ import { CoreStart, IContextContainer, PluginOpaqueId, + PackageInfo, } from '../../../../core/public'; import { ISearchAppMountContext } from './i_search_app_mount_context'; @@ -37,6 +39,7 @@ import { import { TStrategyTypes } from './strategy_types'; import { esSearchService } from './es_search'; import { ISearchGeneric } from './i_search'; +import { getEsClient } from './es_client'; /** * Extends the AppMountContext so other plugins have access @@ -50,6 +53,9 @@ declare module 'kibana/public' { export interface ISearchStart { search: ISearchGeneric; + __LEGACY: { + esClient: any; + }; } /** @@ -74,11 +80,16 @@ export class SearchService implements Plugin { private contextContainer?: IContextContainer>; private search?: ISearchGeneric; + private readonly loadingCount$ = new BehaviorSubject(0); constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): ISearchSetup { - const search = (this.search = createAppMountSearchContext(this.searchStrategies).search); + core.http.addLoadingCountSource(this.loadingCount$); + const search = (this.search = createAppMountSearchContext( + this.searchStrategies, + this.loadingCount$ + ).search); core.application.registerMountContext<'search'>('search', () => { return { search }; }); @@ -115,11 +126,16 @@ export class SearchService implements Plugin { return api; } - public start(core: CoreStart) { + public start(core: CoreStart, packageInfo: PackageInfo) { if (!this.search) { throw new Error('Search should always be defined'); } - return { search: this.search }; + return { + search: this.search, + __LEGACY: { + esClient: getEsClient(core.injectedMetadata, core.http, packageInfo, this.loadingCount$), + }, + }; } public stop() {} diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 76b3283220f673..6a15893f573d83 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -18,7 +18,7 @@ */ import { NotificationsStart } from 'src/core/public'; -import { CoreStart } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { FieldFormatsStart } from '.'; import { createGetterSetter } from '../../kibana_utils/public'; import { IndexPatternsContract } from './index_patterns'; @@ -28,6 +28,12 @@ export const [getNotifications, setNotifications] = createGetterSetter( + 'UiSettings' +); + +export const [getHttp, setHttp] = createGetterSetter('Http'); + export const [getFieldFormats, setFieldFormats] = createGetterSetter( 'FieldFormats' ); @@ -41,3 +47,11 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('Query'); + +export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< + CoreSetup['injectedMetadata'] +>('InjectedMetadata'); + +export const [getSearchService, setSearchService] = createGetterSetter< + DataPublicPluginStart['search'] +>('Search'); diff --git a/src/plugins/data/public/suggestions_provider/types.ts b/src/plugins/data/public/suggestions_provider/types.ts deleted file mode 100644 index a13ecfb10143fe..00000000000000 --- a/src/plugins/data/public/suggestions_provider/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { IFieldType } from '../../common'; - -export type IGetSuggestions = ( - index: string, - field: IFieldType, - query: string, - boolFilter?: any -) => any; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 4fd8bdbaae7b85..d2af2563022482 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -20,10 +20,9 @@ import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public'; -import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.'; +import { AutocompleteSetup, AutocompleteStart } from './autocomplete/types'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats_provider'; import { ISearchSetup, ISearchStart } from './search'; -import { IGetSuggestions } from './suggestions_provider/types'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; import { IndexPatternsContract } from './index_patterns'; @@ -38,15 +37,14 @@ export interface DataStartDependencies { } export interface DataPublicPluginSetup { - autocomplete: AutocompletePublicPluginSetup; + autocomplete: AutocompleteSetup; search: ISearchSetup; fieldFormats: FieldFormatsSetup; query: QuerySetup; } export interface DataPublicPluginStart { - autocomplete: AutocompletePublicPluginStart; - getSuggestions: IGetSuggestions; + autocomplete: AutocompleteStart; indexPatterns: IndexPatternsContract; search: ISearchStart; fieldFormats: FieldFormatsStart; @@ -57,9 +55,6 @@ export interface DataPublicPluginStart { }; } -export * from './autocomplete_provider/types'; -export { IGetSuggestions } from './suggestions_provider/types'; - export interface IDataPluginServices extends Partial { appName: string; uiSettings: CoreStart['uiSettings']; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx index 61290cc16b8a88..2b2d83c9f5a8ba 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/phrase_suggestor.tsx @@ -63,13 +63,19 @@ export class PhraseSuggestorUI extends Component this.updateSuggestions(`${value}`); }; - protected updateSuggestions = debounce(async (value: string = '') => { + protected updateSuggestions = debounce(async (query: string = '') => { const { indexPattern, field } = this.props as PhraseSuggestorProps; if (!field || !this.isSuggestingValues()) { return; } this.setState({ isLoading: true }); - const suggestions = await this.services.data.getSuggestions(indexPattern.title, field, value); + + const suggestions = await this.services.data.autocomplete.getValueSuggestions({ + indexPattern, + field, + query, + }); + this.setState({ suggestions, isLoading: false }); }, 500); } diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 1fb39710f67548..f38adff8920995 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -150,10 +150,16 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -205,6 +211,12 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -304,6 +316,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -776,10 +792,16 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -831,6 +853,12 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -930,6 +958,10 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -1384,10 +1416,16 @@ exports[`QueryStringInput Should pass the query language to the language switche "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -1439,6 +1477,12 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -1538,6 +1582,10 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -2007,10 +2055,16 @@ exports[`QueryStringInput Should pass the query language to the language switche "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -2062,6 +2116,12 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -2161,6 +2221,10 @@ exports[`QueryStringInput Should pass the query language to the language switche }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -2615,10 +2679,16 @@ exports[`QueryStringInput Should render the given query 1`] = ` "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -2670,6 +2740,12 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -2769,6 +2845,10 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -3238,10 +3318,16 @@ exports[`QueryStringInput Should render the given query 1`] = ` "setIsVisible": [MockFunction], }, "data": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "autocomplete": Object { - "addProvider": [MockFunction], - "clearProviders": [MockFunction], - "getProvider": [MockFunction], + "getQuerySuggestions": [MockFunction], + "getValueSuggestions": [MockFunction], + "hasQuerySuggestions": [MockFunction], }, "fieldFormats": Object { "getByFieldType": [MockFunction], @@ -3293,6 +3379,12 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, "search": Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, "search": [MockFunction], }, "ui": Object { @@ -3392,6 +3484,10 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, }, }, + "fatalErrors": Object { + "add": [MockFunction], + "get$": [MockFunction], + }, "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 960a843f98ab90..cf219c35bcced8 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -35,8 +35,7 @@ import { InjectedIntl, injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual } from 'lodash'; import { Toast } from 'src/core/public'; import { - AutocompleteSuggestion, - AutocompleteSuggestionType, + autocomplete, IDataPluginServices, IIndexPattern, PersistedLog, @@ -71,7 +70,7 @@ interface Props { interface State { isSuggestionsVisible: boolean; index: number | null; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; suggestionLimit: number; selectionStart: number | null; selectionEnd: number | null; @@ -90,7 +89,7 @@ const KEY_CODES = { END: 35, }; -const recentSearchType: AutocompleteSuggestionType = 'recentSearch'; +const recentSearchType: autocomplete.QuerySuggestionType = 'recentSearch'; export class QueryStringInputUI extends Component { public state: State = { @@ -138,15 +137,14 @@ export class QueryStringInputUI extends Component { return; } - const uiSettings = this.services.uiSettings; const language = this.props.query.language; const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const autocompleteProvider = this.services.data.autocomplete.getProvider(language); + const hasQuerySuggestions = this.services.data.autocomplete.hasQuerySuggestions(language); if ( - !autocompleteProvider || + !hasQuerySuggestions || !Array.isArray(this.state.indexPatterns) || compact(this.state.indexPatterns).length === 0 ) { @@ -154,10 +152,6 @@ export class QueryStringInputUI extends Component { } const indexPatterns = this.state.indexPatterns; - const getAutocompleteSuggestions = autocompleteProvider({ - config: uiSettings, - indexPatterns, - }); const { selectionStart, selectionEnd } = this.inputRef; if (selectionStart === null || selectionEnd === null) { @@ -167,12 +161,16 @@ export class QueryStringInputUI extends Component { try { if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - const suggestions: AutocompleteSuggestion[] = await getAutocompleteSuggestions({ - query: queryString, - selectionStart, - selectionEnd, - signal: this.abortController.signal, - }); + const suggestions = + (await this.services.data.autocomplete.getQuerySuggestions({ + language, + indexPatterns, + query: queryString, + selectionStart, + selectionEnd, + signal: this.abortController.signal, + })) || []; + return [...suggestions, ...recentSearchSuggestions]; } catch (e) { // TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error @@ -321,7 +319,7 @@ export class QueryStringInputUI extends Component { } }; - private selectSuggestion = (suggestion: AutocompleteSuggestion) => { + private selectSuggestion = (suggestion: autocomplete.QuerySuggestion) => { if (!this.inputRef) { return; } @@ -351,7 +349,7 @@ export class QueryStringInputUI extends Component { } }; - private handleNestedFieldSyntaxNotification = (suggestion: AutocompleteSuggestion) => { + private handleNestedFieldSyntaxNotification = (suggestion: autocomplete.QuerySuggestion) => { if ( 'field' in suggestion && suggestion.field.subType && @@ -453,7 +451,7 @@ export class QueryStringInputUI extends Component { } }; - private onClickSuggestion = (suggestion: AutocompleteSuggestion) => { + private onClickSuggestion = (suggestion: autocomplete.QuerySuggestion) => { if (!this.inputRef) { return; } diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index 591176bf133faf..0c5c701642757d 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -19,14 +19,14 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; import { SuggestionComponent } from './suggestion_component'; const noop = () => { return; }; -const mockSuggestion: AutocompleteSuggestion = { +const mockSuggestion: autocomplete.QuerySuggestion = { description: 'This is not a helpful suggestion', end: 0, start: 42, diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx index fd29de4573ff07..1d2ac8dee1a8a0 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx @@ -20,7 +20,7 @@ import { EuiIcon } from '@elastic/eui'; import classNames from 'classnames'; import React, { FunctionComponent } from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; function getEuiIconType(type: string) { switch (type) { @@ -40,10 +40,10 @@ function getEuiIconType(type: string) { } interface Props { - onClick: (suggestion: AutocompleteSuggestion) => void; + onClick: (suggestion: autocomplete.QuerySuggestion) => void; onMouseEnter: () => void; selected: boolean; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; } diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index 7fb2fdf25104ac..b84f612b6d13a9 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -19,7 +19,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; import { SuggestionComponent } from './suggestion_component'; import { SuggestionsComponent } from './suggestions_component'; @@ -27,7 +27,7 @@ const noop = () => { return; }; -const mockSuggestions: AutocompleteSuggestion[] = [ +const mockSuggestions: autocomplete.QuerySuggestion[] = [ { description: 'This is not a helpful suggestion', end: 0, diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index e4cccbcde4fb8b..b37a2e479e874e 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -19,15 +19,15 @@ import { isEmpty } from 'lodash'; import React, { Component } from 'react'; -import { AutocompleteSuggestion } from '../..'; +import { autocomplete } from '../..'; import { SuggestionComponent } from './suggestion_component'; interface Props { index: number | null; - onClick: (suggestion: AutocompleteSuggestion) => void; + onClick: (suggestion: autocomplete.QuerySuggestion) => void; onMouseEnter: (index: number) => void; show: boolean; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; loadMore: () => void; } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js index 3ec903d5b18e43..8ddd18c2c67f4d 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js @@ -37,7 +37,7 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { describe('conflicts', () => { it('returns a field for each in response, no filtering', () => { const fields = readFieldCapsResponse(esResponse); - expect(fields).toHaveLength(25); + expect(fields).toHaveLength(24); }); it( @@ -68,8 +68,8 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { sandbox.spy(shouldReadFieldFromDocValuesNS, 'shouldReadFieldFromDocValues'); const fields = readFieldCapsResponse(esResponse); const conflictCount = fields.filter(f => f.type === 'conflict').length; - // +1 is for the object field which is filtered out of the final return value from readFieldCapsResponse - sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 1); + // +2 is for the object and nested fields which get filtered out of the final return value from readFieldCapsResponse + sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 2); }); it('converts es types to kibana types', () => { @@ -143,13 +143,6 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { expect(child).toHaveProperty('subType', { nested: { path: 'nested_object_parent' } }); }); - it('returns nested sub-fields as non-aggregatable', () => { - const fields = readFieldCapsResponse(esResponse); - // Normally a keyword field would be aggregatable, but the fact that it is nested overrides that - const child = fields.find(f => f.name === 'nested_object_parent.child.keyword'); - expect(child).toHaveProperty('aggregatable', false); - }); - it('handles fields that are both nested and multi', () => { const fields = readFieldCapsResponse(esResponse); const child = fields.find(f => f.name === 'nested_object_parent.child.keyword'); @@ -159,12 +152,10 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { }); }); - it('returns the nested parent as not searchable or aggregatable', () => { + it('does not include the field actually mapped as nested itself', () => { const fields = readFieldCapsResponse(esResponse); const child = fields.find(f => f.name === 'nested_object_parent'); - expect(child.type).toBe('nested'); - expect(child.aggregatable).toBe(false); - expect(child.searchable).toBe(false); + expect(child).toBeUndefined(); }); it('should not confuse object children for multi or nested field children', () => { diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts index 0c8c2ce48fa844..06eb30db0b24bb 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts @@ -182,19 +182,11 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie if (Object.keys(subType).length > 0) { field.subType = subType; - - // We don't support aggregating on nested fields, trying to do so in the UI will return - // blank results. For now we will stop showing nested fields as an option for aggregation. - // Once we add support for nested fields this condition should be removed and old index - // patterns should be migrated. - if (field.subType.nested) { - field.aggregatable = false; - } } } }); return kibanaFormattedCaps.filter(field => { - return !['object'].includes(field.type); + return !['object', 'nested'].includes(field.type); }); } diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index a2394d88f39314..6ea0799f790fcf 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -65,8 +65,13 @@ describe('Search service', () => { expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: 'yay' }); }); - it('handler throws internal error if the search throws an error', async () => { - const mockSearch = jest.fn().mockRejectedValue('oh no'); + it('handler throws an error if the search throws an error', async () => { + const mockSearch = jest.fn().mockRejectedValue({ + message: 'oh no', + body: { + error: 'oops', + }, + }); const mockContext = { core: { elasticsearch: { @@ -93,7 +98,9 @@ describe('Search service', () => { expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockSearch.mock.calls[0][2]).toBe(mockParams.strategy); - expect(mockResponse.internalError).toBeCalled(); - expect(mockResponse.internalError.mock.calls[0][0]).toEqual({ body: 'oh no' }); + expect(mockResponse.customError).toBeCalled(); + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.body.message).toBe('oh no'); + expect(error.body.attributes.error).toBe('oops'); }); }); diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index eaa72548e08ee7..6f726771c41b2f 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -39,7 +39,15 @@ export function registerSearchRoute(router: IRouter): void { const response = await context.search!.search(searchRequest, {}, strategy); return res.ok({ body: response }); } catch (err) { - return res.internalError({ body: err }); + return res.customError({ + statusCode: err.statusCode, + body: { + message: err.message, + attributes: { + error: err.body.error, + }, + }, + }); } } ); diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.tsx index 7c44fe89d0e7ff..2e199a7471a642 100644 --- a/src/plugins/kibana_react/public/field_icon/field_icon.tsx +++ b/src/plugins/kibana_react/public/field_icon/field_icon.tsx @@ -36,8 +36,8 @@ interface FieldIconProps { | 'number' | '_source' | 'string' - | 'nested' - | string; + | string + | 'nested'; label?: string; size?: IconSize; useColor?: boolean; diff --git a/src/legacy/ui/public/vis/editors/default/agg_params.d.ts b/src/plugins/kibana_utils/public/history/index.ts similarity index 93% rename from src/legacy/ui/public/vis/editors/default/agg_params.d.ts rename to src/plugins/kibana_utils/public/history/index.ts index 89896c0e1be3e8..b4b5658c1c886e 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_params.d.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -17,6 +17,4 @@ * under the License. */ -export interface AggParams { - [key: string]: unknown; -} +export { removeQueryParam } from './remove_query_param'; diff --git a/src/plugins/kibana_utils/public/history/remove_query_param.test.ts b/src/plugins/kibana_utils/public/history/remove_query_param.test.ts new file mode 100644 index 00000000000000..0b2547ae94668a --- /dev/null +++ b/src/plugins/kibana_utils/public/history/remove_query_param.test.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { removeQueryParam } from './remove_query_param'; +import { createMemoryHistory, Location } from 'history'; + +describe('removeQueryParam', () => { + it('should remove query param from url', () => { + const startLocation: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: "?_a=(description:'')&_b=3", + state: null, + hash: '', + }; + + const history = createMemoryHistory(); + history.push(startLocation); + removeQueryParam(history, '_a'); + + expect(history.location).toEqual( + expect.objectContaining({ + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: '?_b=3', + state: null, + hash: '', + }) + ); + }); + + it('should not fail if nothing to remove', () => { + const startLocation: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: "?_a=(description:'')&_b=3", + state: null, + hash: '', + }; + + const history = createMemoryHistory(); + history.push(startLocation); + removeQueryParam(history, '_c'); + + expect(history.location).toEqual(expect.objectContaining(startLocation)); + }); + + it('should not fail if no search', () => { + const startLocation: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: '', + state: null, + hash: '', + }; + + const history = createMemoryHistory(); + history.push(startLocation); + removeQueryParam(history, '_c'); + + expect(history.location).toEqual(expect.objectContaining(startLocation)); + }); +}); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js b/src/plugins/kibana_utils/public/history/remove_query_param.ts similarity index 54% rename from src/legacy/server/url_shortening/routes/lib/short_url_lookup.js rename to src/plugins/kibana_utils/public/history/remove_query_param.ts index a8a01d1433a7a3..fbf985998b4cd1 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js +++ b/src/plugins/kibana_utils/public/history/remove_query_param.ts @@ -17,27 +17,23 @@ * under the License. */ -import { get } from 'lodash'; +import { History, Location } from 'history'; +import { parse } from 'querystring'; +import { stringifyQueryString } from '../state_management/url/stringify_query_string'; // TODO: extract it to ../url -export function shortUrlLookupProvider(server) { - async function updateMetadata(doc, req) { - try { - await req.getSavedObjectsClient().update('url', doc.id, { - accessDate: new Date(), - accessCount: get(doc, 'attributes.accessCount', 0) + 1, - }); - } catch (err) { - server.log('Warning: Error updating url metadata', err); - //swallow errors. It isn't critical if there is no update. - } - } - - return { - async getUrl(id, req) { - const doc = await req.getSavedObjectsClient().get('url', id); - updateMetadata(doc, req); - - return doc.attributes.url; - }, +export function removeQueryParam(history: History, param: string, replace: boolean = true) { + const oldLocation = history.location; + const search = (oldLocation.search || '').replace(/^\?/, ''); + const query = parse(search); + delete query[param]; + const newSearch = stringifyQueryString(query); + const newLocation: Location = { + ...oldLocation, + search: newSearch, }; + if (replace) { + history.replace(newLocation); + } else { + history.push(newLocation); + } } diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index fa58a61e51232b..00c1c95028b4dc 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -58,3 +58,5 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; +export { removeQueryParam } from './history'; +export { applyDiff } from './state_management/utils/diff_object'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index f1c527d3d53097..6e4c505c62ebc8 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -85,6 +85,7 @@ describe('kbn_url_storage', () => { beforeEach(() => { history = createMemoryHistory(); urlControls = createKbnUrlControls(history); + urlControls.update('/', true); }); const getCurrentUrl = () => createPath(history.location); @@ -143,17 +144,6 @@ describe('kbn_url_storage', () => { expect(cb).toHaveBeenCalledTimes(3); }); - it('should flush async url updates', async () => { - const pr1 = urlControls.updateAsync(() => '/1', false); - const pr2 = urlControls.updateAsync(() => '/2', false); - const pr3 = urlControls.updateAsync(() => '/3', false); - expect(getCurrentUrl()).toBe('/'); - urlControls.flush(); - expect(getCurrentUrl()).toBe('/3'); - await Promise.all([pr1, pr2, pr3]); - expect(getCurrentUrl()).toBe('/3'); - }); - it('flush should take priority over regular replace behaviour', async () => { const pr1 = urlControls.updateAsync(() => '/1', true); const pr2 = urlControls.updateAsync(() => '/2', false); @@ -174,6 +164,48 @@ describe('kbn_url_storage', () => { await Promise.all([pr1, pr2, pr3]); expect(getCurrentUrl()).toBe('/'); }); + + it('should retrieve pending url ', async () => { + const pr1 = urlControls.updateAsync(() => '/1', true); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', true); + expect(urlControls.getPendingUrl()).toEqual('/3'); + expect(getCurrentUrl()).toBe('/'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + + expect(urlControls.getPendingUrl()).toBeUndefined(); + }); + }); + + describe('urlControls - browser history integration', () => { + let history: History; + let urlControls: IKbnUrlControls; + beforeEach(() => { + history = createBrowserHistory(); + urlControls = createKbnUrlControls(history); + urlControls.update('/', true); + }); + + const getCurrentUrl = () => window.location.href; + + it('should flush async url updates', async () => { + const pr1 = urlControls.updateAsync(() => '/1', false); + const pr2 = urlControls.updateAsync(() => '/2', false); + const pr3 = urlControls.updateAsync(() => '/3', false); + expect(getCurrentUrl()).toBe('http://localhost/'); + expect(urlControls.flush()).toBe('http://localhost/3'); + expect(getCurrentUrl()).toBe('http://localhost/3'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('http://localhost/3'); + }); + + it('flush() should return undefined, if no url updates happened', () => { + expect(urlControls.flush()).toBeUndefined(); + urlControls.updateAsync(() => 'http://localhost/1', false); + urlControls.updateAsync(() => 'http://localhost/', false); + expect(urlControls.flush()).toBeUndefined(); + }); }); describe('getRelativeToHistoryPath', () => { diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 03c136ea3d0928..1dd204e7172132 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -107,25 +107,34 @@ export interface IKbnUrlControls { listen: (cb: () => void) => () => void; /** - * Updates url synchronously + * Updates url synchronously, if needed + * skips the update and returns undefined in case when trying to update to current url + * otherwise returns new url + * * @param url - url to update to * @param replace - use replace instead of push */ - update: (url: string, replace: boolean) => string; + update: (url: string, replace: boolean) => string | undefined; /** * Schedules url update to next microtask, * Useful to batch sync changes to url to cause only one browser history update * @param updater - fn which receives current url and should return next url to update to * @param replace - use replace instead of push + * */ - updateAsync: (updater: UrlUpdaterFnType, replace?: boolean) => Promise; + updateAsync: (updater: UrlUpdaterFnType, replace?: boolean) => Promise; /** - * Synchronously flushes scheduled url updates + * If there is a pending url update - returns url that is scheduled for update + */ + getPendingUrl: () => string | undefined; + + /** + * Synchronously flushes scheduled url updates. Returns new flushed url, if there was an update. Otherwise - undefined. * @param replace - if replace passed in, then uses it instead of push. Otherwise push or replace is picked depending on updateQueue */ - flush: (replace?: boolean) => string; + flush: (replace?: boolean) => string | undefined; /** * Cancels any pending url updates @@ -143,9 +152,9 @@ export const createKbnUrlControls = ( // if any call in a queue asked to push, then we should push let shouldReplace = true; - function updateUrl(newUrl: string, replace = false): string { + function updateUrl(newUrl: string, replace = false): string | undefined { const currentUrl = getCurrentUrl(); - if (newUrl === currentUrl) return currentUrl; // skip update + if (newUrl === currentUrl) return undefined; // skip update const historyPath = getRelativeToHistoryPath(newUrl, history); @@ -166,15 +175,22 @@ export const createKbnUrlControls = ( // runs scheduled url updates function flush(replace = shouldReplace) { - if (updateQueue.length === 0) return getCurrentUrl(); - const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + const nextUrl = getPendingUrl(); - cleanUp(); + if (!nextUrl) return; - const newUrl = updateUrl(resultUrl, replace); + cleanUp(); + const newUrl = updateUrl(nextUrl, replace); return newUrl; } + function getPendingUrl() { + if (updateQueue.length === 0) return undefined; + const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + + return resultUrl; + } + return { listen: (cb: () => void) => history.listen(() => { @@ -199,6 +215,9 @@ export const createKbnUrlControls = ( cancel: () => { cleanUp(); }, + getPendingUrl: () => { + return getPendingUrl(); + }, }; }; diff --git a/src/legacy/ui/public/state_management/utils/diff_object.test.ts b/src/plugins/kibana_utils/public/state_management/utils/diff_object.test.ts similarity index 100% rename from src/legacy/ui/public/state_management/utils/diff_object.test.ts rename to src/plugins/kibana_utils/public/state_management/utils/diff_object.test.ts diff --git a/src/legacy/ui/public/state_management/utils/diff_object.ts b/src/plugins/kibana_utils/public/state_management/utils/diff_object.ts similarity index 100% rename from src/legacy/ui/public/state_management/utils/diff_object.ts rename to src/plugins/kibana_utils/public/state_management/utils/diff_object.ts diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 08ad1551420d2e..17f41483a0a21e 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -291,6 +291,42 @@ describe('state_sync', () => { stop(); }); + + it("should preserve reference to unchanged state slices if them didn't change", async () => { + const otherUnchangedSlice = { a: 'test' }; + const oldState = { + todos: container.get().todos, + otherUnchangedSlice, + }; + container.set(oldState as any); + + const { stop, start } = syncStates([ + { + stateContainer: withDefaultState(container, defaultState), + storageKey: key, + stateStorage: urlSyncStrategy, + }, + ]); + await urlSyncStrategy.set('_s', container.get()); + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_s=(otherUnchangedSlice:(a:test),todos:!((completed:!f,id:0,text:'Learning%20state%20containers')))"` + ); + start(); + + history.replace( + "/#?_s=(otherUnchangedSlice:(a:test),todos:!((completed:!t,id:0,text:'Learning%20state%20containers')))" + ); + + const newState = container.get(); + expect(newState.todos).toEqual([ + { id: 0, text: 'Learning state containers', completed: true }, + ]); + + // reference to unchanged slice is preserved + expect((newState as any).otherUnchangedSlice).toBe(otherUnchangedSlice); + + stop(); + }); }); }); 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 9c1116e5da5318..28d133829e07c4 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -24,6 +24,7 @@ import { IStateSyncConfig } from './types'; import { IStateStorage } from './state_sync_state_storage'; import { distinctUntilChangedWithInitialValue } from '../../common'; import { BaseState } from '../state_containers'; +import { applyDiff } from '../state_management/utils/diff_object'; /** * Utility for syncing application state wrapped in state container @@ -100,7 +101,18 @@ export function syncState< const updateState = () => { const newState = stateStorage.get(storageKey); const oldState = stateContainer.get(); - if (!defaultComparator(newState, oldState)) { + if (newState) { + // apply only real differences to new state + const mergedState = { ...oldState } as State; + // merges into 'mergedState' all differences from newState, + // but leaves references if they are deeply the same + const diff = applyDiff(mergedState, newState); + + if (diff.keys.length > 0) { + stateContainer.set(mergedState); + } + } else if (oldState !== newState) { + // empty new state case stateContainer.set(newState); } }; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index 826122176e061e..cc3f1df7c1e00a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -46,9 +46,11 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; urlStateStorage.set(key, state); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/"`); - urlStateStorage.flush(); + expect(urlStateStorage.flush()).toBe(true); expect(getCurrentUrl()).toMatchInlineSnapshot(`"/#?_s=(ok:1,test:test)"`); expect(urlStateStorage.get(key)).toEqual(state); + + expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update }); it('should cancel url updates', async () => { @@ -62,6 +64,19 @@ describe('KbnUrlStateStorage', () => { expect(urlStateStorage.get(key)).toEqual(null); }); + it('should cancel url updates if synchronously returned to the same state', async () => { + const state1 = { test: 'test', ok: 1 }; + const state2 = { test: 'test', ok: 2 }; + const key = '_s'; + const pr1 = urlStateStorage.set(key, state1); + await pr1; + const historyLength = history.length; + const pr2 = urlStateStorage.set(key, state2); + const pr3 = urlStateStorage.set(key, state1); + await Promise.all([pr2, pr3]); + expect(history.length).toBe(historyLength); + }); + it('should notify about url changes', async () => { expect(urlStateStorage.change$).toBeDefined(); const key = '_s'; 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 245006349ad55b..082eaa5095ab94 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 @@ -28,7 +28,11 @@ import { } from '../../state_management/url'; export interface IKbnUrlStateStorage extends IStateStorage { - set: (key: string, state: State, opts?: { replace: boolean }) => Promise; + set: ( + key: string, + state: State, + opts?: { replace: boolean } + ) => Promise; get: (key: string) => State | null; change$: (key: string) => Observable; @@ -36,7 +40,8 @@ export interface IKbnUrlStateStorage extends IStateStorage { cancel: () => void; // synchronously runs any pending url updates - flush: (opts?: { replace?: boolean }) => void; + // returned boolean indicates if change occurred + flush: (opts?: { replace?: boolean }) => boolean; } /** @@ -60,7 +65,11 @@ export const createKbnUrlStateStorage = ( replace ); }, - get: key => getStateFromKbnUrl(key), + get: key => { + // if there is a pending url update, then state will be extracted from that pending url, + // otherwise current url will be used to retrieve state from + return getStateFromKbnUrl(key, url.getPendingUrl()); + }, change$: (key: string) => new Observable(observer => { const unlisten = url.listen(() => { @@ -75,7 +84,7 @@ export const createKbnUrlStateStorage = ( share() ), flush: ({ replace = false }: { replace?: boolean } = {}) => { - url.flush(replace); + return !!url.flush(replace); }, cancel() { url.cancel(); diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index cb0b82d0f0bde4..69ba813d2347ed 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -161,14 +161,15 @@ export class ManagementSidebarNav extends React.Component< } public render() { - const HEADER_ID = 'management-nav-header'; + const HEADER_ID = 'stack-management-nav-header'; return ( <>

{i18n.translate('management.nav.label', { - defaultMessage: 'Management', + // todo + defaultMessage: 'Stack Management', })}

diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index faec466dbd6714..4ece75bbf36da3 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -24,7 +24,12 @@ export function plugin(initializerContext: PluginInitializerContext) { return new ManagementPlugin(); } -export { ManagementSetup, ManagementStart, RegisterManagementApp } from './types'; +export { + ManagementSetup, + ManagementStart, + RegisterManagementApp, + RegisterManagementAppArgs, +} from './types'; export { ManagementApp } from './management_app'; export { ManagementSection } from './management_section'; export { ManagementSidebarNav } from './components'; // for use in legacy management apps diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index 63d919377f89ea..ca35db56c340be 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -27,7 +27,8 @@ export class LegacyManagementAdapter { 'management', { display: i18n.translate('management.displayName', { - defaultMessage: 'Management', + // todo + defaultMessage: 'Stack Management', }), }, capabilities @@ -35,6 +36,7 @@ export class LegacyManagementAdapter { this.main.register('data', { display: i18n.translate('management.connectDataDisplayName', { + // todo defaultMessage: 'Connect Data', }), order: 0, diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index f7e8dba4f82100..02b3ea306c23d8 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -34,7 +34,7 @@ export class ManagementApp { readonly basePath: string; readonly order: number; readonly mount: ManagementSectionMount; - protected enabledStatus: boolean = true; + private enabledStatus = true; constructor( { id, title, basePath, order = 100, mount }: CreateManagementApp, @@ -54,12 +54,18 @@ export class ManagementApp { title, mount: async ({}, params) => { let appUnmount: Unmount; + if (!this.enabledStatus) { + const [coreStart] = await getStartServices(); + coreStart.application.navigateToApp('kibana#/management'); + return () => {}; + } async function setBreadcrumbs(crumbs: ChromeBreadcrumb[]) { const [coreStart] = await getStartServices(); coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - defaultMessage: 'Management', + // todo + defaultMessage: 'Stack Management', }), href: '#/management', }, diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index 5ea5e5b324717d..c4e042fe452f9c 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -23,7 +23,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { NewsfeedPluginInjectedConfig } from '../types'; +import { FetchResult, NewsfeedPluginInjectedConfig } from '../types'; import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button'; import { getApi } from './lib/api'; @@ -54,10 +54,14 @@ export class NewsfeedPublicPlugin implements Plugin { private fetchNewsfeed(core: CoreStart) { const { http, injectedMetadata } = core; - const config = injectedMetadata.getInjectedVar( - 'newsfeed' - ) as NewsfeedPluginInjectedConfig['newsfeed']; + const config = injectedMetadata.getInjectedVar('newsfeed') as + | NewsfeedPluginInjectedConfig['newsfeed'] + | undefined; + if (!config) { + // running in new platform, injected metadata not available + return new Rx.Observable(); + } return getApi(http, config, this.kibanaVersion).pipe( takeUntil(this.stop$), // stop the interval when stop method is called catchError(() => Rx.of(null)) // do not throw error diff --git a/src/plugins/share/common/short_url_routes.ts b/src/plugins/share/common/short_url_routes.ts new file mode 100644 index 00000000000000..7b42534de2ab15 --- /dev/null +++ b/src/plugins/share/common/short_url_routes.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const GOTO_PREFIX = '/goto'; + +export const getUrlIdFromGotoRoute = (path: string) => + path.match(new RegExp(`${GOTO_PREFIX}/(.*)$`))?.[1]; + +export const getGotoPath = (urlId: string) => `${GOTO_PREFIX}/${urlId}`; + +export const GETTER_PREFIX = '/api/short_url'; + +export const getUrlPath = (urlId: string) => `${GETTER_PREFIX}/${urlId}`; + +export const CREATE_PATH = '/api/shorten_url'; diff --git a/src/plugins/share/public/lib/url_shortener.ts b/src/plugins/share/public/lib/url_shortener.ts index 29d91bdb1aae6a..f2259f1fabf7d8 100644 --- a/src/plugins/share/public/lib/url_shortener.ts +++ b/src/plugins/share/public/lib/url_shortener.ts @@ -19,6 +19,7 @@ import url from 'url'; import { HttpStart } from 'kibana/public'; +import { CREATE_PATH, getGotoPath } from '../../common/short_url_routes'; export async function shortenUrl( absoluteUrl: string, @@ -34,10 +35,10 @@ export async function shortenUrl( const body = JSON.stringify({ url: relativeUrl }); - const resp = await post('/api/shorten_url', { body }); + const resp = await post(CREATE_PATH, { body }); return url.format({ protocol: parsedUrl.protocol, host: parsedUrl.host, - pathname: `${basePath}/goto/${resp.urlId}`, + pathname: `${basePath}${getGotoPath(resp.urlId)}`, }); } diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts index 5610490be33b33..730814fe9ed237 100644 --- a/src/plugins/share/public/plugin.test.ts +++ b/src/plugins/share/public/plugin.test.ts @@ -20,6 +20,7 @@ import { registryMock, managerMock } from './plugin.test.mocks'; import { SharePlugin } from './plugin'; import { CoreStart } from 'kibana/public'; +import { coreMock } from '../../../core/public/mocks'; describe('SharePlugin', () => { beforeEach(() => { @@ -30,16 +31,28 @@ describe('SharePlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new SharePlugin().setup(); + const coreSetup = coreMock.createSetup(); + const setup = await new SharePlugin().setup(coreSetup); expect(registryMock.setup).toHaveBeenCalledWith(); expect(setup.register).toBeDefined(); }); + + test('registers redirect app', async () => { + const coreSetup = coreMock.createSetup(); + await new SharePlugin().setup(coreSetup); + expect(coreSetup.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'short_url_redirect', + }) + ); + }); }); describe('start', () => { test('wires up and returns show function, but not registry', async () => { + const coreSetup = coreMock.createSetup(); const service = new SharePlugin(); - await service.setup(); + await service.setup(coreSetup); const start = await service.start({} as CoreStart); expect(registryMock.start).toHaveBeenCalled(); expect(managerMock.start).toHaveBeenCalledWith( diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 6d78211cf99544..01c248624950ab 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -17,15 +17,17 @@ * under the License. */ -import { CoreStart, Plugin } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ShareMenuManager, ShareMenuManagerStart } from './services'; import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; +import { createShortUrlRedirectApp } from './services/short_url_redirect_app'; export class SharePlugin implements Plugin { private readonly shareMenuRegistry = new ShareMenuRegistry(); private readonly shareContextMenu = new ShareMenuManager(); - public async setup() { + public async setup(core: CoreSetup) { + core.application.register(createShortUrlRedirectApp(core, window.location)); return { ...this.shareMenuRegistry.setup(), }; diff --git a/src/plugins/share/public/services/short_url_redirect_app.test.ts b/src/plugins/share/public/services/short_url_redirect_app.test.ts new file mode 100644 index 00000000000000..206e637451ec07 --- /dev/null +++ b/src/plugins/share/public/services/short_url_redirect_app.test.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createShortUrlRedirectApp } from './short_url_redirect_app'; +import { coreMock } from '../../../../core/public/mocks'; +import { hashUrl } from '../../../kibana_utils/public'; + +jest.mock('../../../kibana_utils/public', () => ({ hashUrl: jest.fn(x => `${x}/hashed`) })); + +describe('short_url_redirect_app', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch url and redirect to hashed version', async () => { + const coreSetup = coreMock.createSetup({ basePath: 'base' }); + coreSetup.http.get.mockResolvedValueOnce({ url: '/app/abc' }); + const locationMock = { pathname: '/base/goto/12345', href: '' } as Location; + + const { mount } = createShortUrlRedirectApp(coreSetup, locationMock); + await mount(); + + // check for fetching the complete URL + expect(coreSetup.http.get).toHaveBeenCalledWith('/api/short_url/12345'); + // check for hashing the URL returned from the server + expect(hashUrl).toHaveBeenCalledWith('/app/abc'); + // check for redirecting to the prepended path + expect(locationMock.href).toEqual('base/app/abc/hashed'); + }); +}); diff --git a/src/plugins/share/public/services/short_url_redirect_app.ts b/src/plugins/share/public/services/short_url_redirect_app.ts new file mode 100644 index 00000000000000..6f72b711f66020 --- /dev/null +++ b/src/plugins/share/public/services/short_url_redirect_app.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup } from 'kibana/public'; +import { getUrlIdFromGotoRoute, getUrlPath, GOTO_PREFIX } from '../../common/short_url_routes'; +import { hashUrl } from '../../../kibana_utils/public'; + +export const createShortUrlRedirectApp = (core: CoreSetup, location: Location) => ({ + id: 'short_url_redirect', + appRoute: GOTO_PREFIX, + chromeless: true, + title: 'Short URL Redirect', + async mount() { + const urlId = getUrlIdFromGotoRoute(location.pathname); + + if (!urlId) { + throw new Error('Url id not present in path'); + } + + const response = await core.http.get<{ url: string }>(getUrlPath(urlId)); + const redirectUrl = response.url; + const hashedUrl = hashUrl(redirectUrl); + const url = core.http.basePath.prepend(hashedUrl); + + location.href = url; + + return () => {}; + }, +}); diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts index bd4b6fdb08791d..22d10c25197c90 100644 --- a/src/plugins/share/server/routes/create_routes.ts +++ b/src/plugins/share/server/routes/create_routes.ts @@ -22,11 +22,13 @@ import { CoreSetup, Logger } from 'kibana/server'; import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; import { createShortenUrlRoute } from './shorten_url'; +import { createGetterRoute } from './get'; export function createRoutes({ http }: CoreSetup, logger: Logger) { const shortUrlLookup = shortUrlLookupProvider({ logger }); const router = http.createRouter(); createGotoRoute({ router, shortUrlLookup, http }); + createGetterRoute({ router, shortUrlLookup, http }); createShortenUrlRoute({ router, shortUrlLookup }); } diff --git a/src/legacy/server/url_shortening/routes/goto.js b/src/plugins/share/server/routes/get.ts similarity index 55% rename from src/legacy/server/url_shortening/routes/goto.js rename to src/plugins/share/server/routes/get.ts index 7a874786423d81..d6b191341dbe14 100644 --- a/src/legacy/server/url_shortening/routes/goto.js +++ b/src/plugins/share/server/routes/get.ts @@ -17,23 +17,40 @@ * under the License. */ -import { handleShortUrlError } from './lib/short_url_error'; +import { CoreSetup, IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { getUrlPath } from '../../common/short_url_routes'; -export const createGotoRoute = ({ server, shortUrlLookup }) => ({ - method: 'GET', - path: '/goto_LP/{urlId}', - handler: async function(request, h) { - try { - const url = await shortUrlLookup.getUrl(request.params.urlId, request); +export const createGetterRoute = ({ + router, + shortUrlLookup, + http, +}: { + router: IRouter; + shortUrlLookup: ShortUrlLookupService; + http: CoreSetup['http']; +}) => { + router.get( + { + path: getUrlPath('{urlId}'), + validate: { + params: schema.object({ urlId: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); shortUrlAssertValid(url); - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); - return h.renderApp(app, { - redirectUrl: url, + return response.ok({ + body: { + url, + }, }); - } catch (err) { - throw handleShortUrlError(err); - } - }, -}); + }) + ); +}; diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 7343dc1bd34a26..5c3a4e441099fb 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { getGotoPath } from '../../common/short_url_routes'; export const createGotoRoute = ({ router, @@ -34,7 +35,7 @@ export const createGotoRoute = ({ }) => { router.get( { - path: '/goto/{urlId}', + path: getGotoPath('{urlId}'), validate: { params: schema.object({ urlId: schema.string() }), }, @@ -54,10 +55,13 @@ export const createGotoRoute = ({ }, }); } - return response.redirected({ + const body = await context.core.rendering.render(); + + return response.ok({ headers: { - location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + 'content-security-policy': http.csp.header, }, + body, }); }) ); diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index 116b90c6971c5c..41570f8a5f453a 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { CREATE_PATH } from '../../common/short_url_routes'; export const createShortenUrlRoute = ({ shortUrlLookup, @@ -32,7 +33,7 @@ export const createShortenUrlRoute = ({ }) => { router.post( { - path: '/api/shorten_url', + path: CREATE_PATH, validate: { body: schema.object({ url: schema.string() }), }, diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 0c57c6139d2984..1d8667840faba0 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -25,4 +25,8 @@ module.exports = { allowedStartRules: ['start', 'Literal'], }, }, + timelion_chain: { + src: 'src/legacy/core_plugins/vis_type_timelion/public/chain.peg', + dest: 'src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js', + }, }; diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index 555056173ec62f..c4c71abdae1250 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -72,15 +72,7 @@ export default function({ getService }) { readFromDocValues: true, }, { - aggregatable: false, - esTypes: ['nested'], - name: 'nestedField', - readFromDocValues: false, - searchable: false, - type: 'nested', - }, - { - aggregatable: false, + aggregatable: true, esTypes: ['keyword'], name: 'nestedField.child', readFromDocValues: true, @@ -162,15 +154,7 @@ export default function({ getService }) { readFromDocValues: true, }, { - aggregatable: false, - esTypes: ['nested'], - name: 'nestedField', - readFromDocValues: false, - searchable: false, - type: 'nested', - }, - { - aggregatable: false, + aggregatable: true, esTypes: ['keyword'], name: 'nestedField.child', readFromDocValues: true, diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index 90f02c36b3b7fa..0b628100a98bd6 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -34,6 +34,7 @@ export default function({ getService, getPageObjects }) { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + pageNavigation: 'individual', }); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); @@ -83,7 +84,7 @@ export default function({ getService, getPageObjects }) { describe('is false', () => { before(async () => { - await PageObjects.header.clickManagement(); + await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs'); }); @@ -98,7 +99,7 @@ export default function({ getService, getPageObjects }) { }); after(async () => { - await PageObjects.header.clickManagement(); + await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs'); await PageObjects.header.clickDashboard(); diff --git a/test/functional/apps/dashboard/dashboard_clone.js b/test/functional/apps/dashboard/dashboard_clone.js index f5485c1db206e7..8b7f6ba6a34dd3 100644 --- a/test/functional/apps/dashboard/dashboard_clone.js +++ b/test/functional/apps/dashboard/dashboard_clone.js @@ -37,7 +37,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.addVisualizations( PageObjects.dashboard.getTestVisualizationNames() ); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName); await PageObjects.dashboard.clickClone(); await PageObjects.dashboard.confirmClone(); diff --git a/test/functional/apps/discover/_errors.js b/test/functional/apps/discover/_errors.js index 53dcd8cc9e5c14..7dbb93c884f46d 100644 --- a/test/functional/apps/discover/_errors.js +++ b/test/functional/apps/discover/_errors.js @@ -22,10 +22,11 @@ import expect from '@kbn/expect'; export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'discover']); describe('errors', function describeIndexTests() { before(async function() { + await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('invalid_scripted_field'); await PageObjects.common.navigateToApp('discover'); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index a32024adb5ec77..e685c43e9ce98a 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) { describe('index pattern filter', function describeIndexTests() { before(async function() { // delete .kibana index and then wait for Kibana to re-create it - await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); }); diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index 84f955d9c78792..d989f8e2539a00 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -37,6 +37,7 @@ export default function({ getService, getPageObjects }) { it('should update table header when columns change', async function() { await inspector.open(); await inspector.expectTableHeaders(['Count']); + await inspector.close(); log.debug('Add Average Metric on machine.ram field'); await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); @@ -45,6 +46,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableHeaders(['Count', 'Average machine.ram']); + await inspector.close(); }); describe('filtering on inspector table values', function() { diff --git a/test/functional/apps/visualize/_lab_mode.js b/test/functional/apps/visualize/_lab_mode.js index 3ee806af8165db..b082480d95a2ee 100644 --- a/test/functional/apps/visualize/_lab_mode.js +++ b/test/functional/apps/visualize/_lab_mode.js @@ -23,7 +23,6 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']); - // Flaky: https://github.com/elastic/kibana/issues/19743 describe('visualize lab mode', () => { it('disabling does not break loading saved searches', async () => { await PageObjects.common.navigateToUrl('discover', ''); @@ -36,7 +35,7 @@ export default function({ getService, getPageObjects }) { log.info('found saved search before toggling enableLabs mode'); // Navigate to advanced setting and disable lab mode - await PageObjects.header.clickManagement(); + await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.toggleAdvancedSettingCheckbox('visualize:enableLabs'); @@ -50,7 +49,7 @@ export default function({ getService, getPageObjects }) { after(async () => { await PageObjects.discover.closeLoadSaveSearchPanel(); - await PageObjects.header.clickManagement(); + await PageObjects.header.clickStackManagement(); await PageObjects.settings.clickKibanaSettings(); await PageObjects.settings.clearAdvancedSettings('visualize:enableLabs'); }); diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js index 51c03c90f507b8..fee6c074af5d25 100644 --- a/test/functional/apps/visualize/_markdown_vis.js +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -63,7 +63,7 @@ export default function({ getPageObjects, getService }) { }); it('should resize the editor', async function() { - const editorSidebar = await find.byCssSelector('.visEditor__sidebar'); + const editorSidebar = await find.byCssSelector('.visEditor__collapsibleSidebar'); const initialSize = await editorSidebar.getSize(); await PageObjects.visEditor.sizeUpEditor(); const afterSize = await editorSidebar.getSize(); diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index e7ce5808554b47..d0f7810b6f8bb3 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -57,7 +57,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.selectField('machine.ram', 'metrics'); // go to options page log.debug('Going to axis options'); - await pointSeriesVis.clickAxisOptions(); + await PageObjects.visEditor.clickMetricsAndAxes(); // add another value axis log.debug('adding axis'); await pointSeriesVis.clickAddAxis(); diff --git a/test/functional/apps/visualize/_region_map.js b/test/functional/apps/visualize/_region_map.js index 10cbd9913c70c2..2467a540616430 100644 --- a/test/functional/apps/visualize/_region_map.js +++ b/test/functional/apps/visualize/_region_map.js @@ -57,6 +57,7 @@ export default function({ getService, getPageObjects }) { ]; await inspector.open(); await inspector.expectTableData(expectedData); + await inspector.close(); }); it('should change results after changing layer to world', async function() { @@ -94,6 +95,8 @@ export default function({ getService, getPageObjects }) { ['BR', '415'], ]; expect(actualData).to.eql(expectedData); + + await inspector.close(); }); it('should contain a dropdown with the default road_map base layer as an option', async () => { diff --git a/test/functional/apps/visualize/_tag_cloud.js b/test/functional/apps/visualize/_tag_cloud.js index 4f921cec1fdf10..a527e9bcad42ff 100644 --- a/test/functional/apps/visualize/_tag_cloud.js +++ b/test/functional/apps/visualize/_tag_cloud.js @@ -77,12 +77,12 @@ export default function({ getService, getPageObjects }) { }); it('should collapse the sidebar', async function() { - const editorSidebar = await find.byCssSelector('.collapsible-sidebar'); + const editorSidebar = await find.byCssSelector('.visEditorSidebar'); await PageObjects.visEditor.clickEditorSidebarCollapse(); // Give d3 tag cloud some time to rearrange tags await PageObjects.common.sleep(1000); - const afterSize = await editorSidebar.getSize(); - expect(afterSize.width).to.be(0); + const isDisplayed = await editorSidebar.isDisplayed(); + expect(isDisplayed).to.be(false); await PageObjects.visEditor.clickEditorSidebarCollapse(); }); diff --git a/test/functional/page_objects/header_page.js b/test/functional/page_objects/header_page.js index f82e4e4387e270..05edd64545a565 100644 --- a/test/functional/page_objects/header_page.js +++ b/test/functional/page_objects/header_page.js @@ -59,8 +59,8 @@ export function HeaderPageProvider({ getService, getPageObjects }) { await this.awaitGlobalLoadingIndicatorHidden(); } - async clickManagement() { - await appsMenu.clickLink('Management'); + async clickStackManagement() { + await appsMenu.clickLink('Stack Management'); await this.awaitGlobalLoadingIndicatorHidden(); } diff --git a/test/functional/page_objects/point_series_page.js b/test/functional/page_objects/point_series_page.js index 74bf07b59bc381..594facb8b74b5b 100644 --- a/test/functional/page_objects/point_series_page.js +++ b/test/functional/page_objects/point_series_page.js @@ -23,10 +23,6 @@ export function PointSeriesPageProvider({ getService }) { const find = getService('find'); class PointSeriesVis { - async clickAxisOptions() { - return await testSubjects.click('visEditorTabadvanced'); - } - async clickAddAxis() { return await testSubjects.click('visualizeAddYAxisButton'); } diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.ts similarity index 90% rename from test/functional/page_objects/settings_page.js rename to test/functional/page_objects/settings_page.ts index a4ae361b12ed83..e92780143f09a6 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.ts @@ -19,8 +19,10 @@ import { map as mapAsync } from 'bluebird'; import expect from '@kbn/expect'; +import { NavSetting } from '../../../src/core/public/chrome/ui/header/'; +import { FtrProviderContext } from '../ftr_provider_context'; -export function SettingsPageProvider({ getService, getPageObjects }) { +export function SettingsPageProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -34,7 +36,8 @@ export function SettingsPageProvider({ getService, getPageObjects }) { async clickNavigation() { find.clickDisplayedByCssSelector('.app-link:nth-child(5) a'); } - async clickLinkText(text) { + + async clickLinkText(text: string) { await find.clickByDisplayedLinkText(text); } async clickKibanaSettings() { @@ -55,6 +58,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { // check for the index pattern info flyout that covers the // create index pattern button on smaller screens + // @ts-ignore await retry.waitFor('index pattern info flyout', async () => { if (await testSubjects.exists('CreateIndexPatternPrompt')) { await testSubjects.click('CreateIndexPatternPrompt > euiFlyoutCloseButton'); @@ -62,18 +66,18 @@ export function SettingsPageProvider({ getService, getPageObjects }) { }); } - async getAdvancedSettings(propertyName) { + async getAdvancedSettings(propertyName: string) { log.debug('in getAdvancedSettings'); const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); return await setting.getAttribute('value'); } - async expectDisabledAdvancedSetting(propertyName) { + async expectDisabledAdvancedSetting(propertyName: string) { const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); expect(setting.getAttribute('disabled')).to.eql(''); } - async getAdvancedSettingCheckbox(propertyName) { + async getAdvancedSettingCheckbox(propertyName: string) { log.debug('in getAdvancedSettingCheckbox'); return await testSubjects.getAttribute( `advancedSetting-editField-${propertyName}`, @@ -81,12 +85,12 @@ export function SettingsPageProvider({ getService, getPageObjects }) { ); } - async clearAdvancedSettings(propertyName) { + async clearAdvancedSettings(propertyName: string) { await testSubjects.click(`advancedSetting-resetField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); } - async setAdvancedSettingsSelect(propertyName, propertyValue) { + async setAdvancedSettingsSelect(propertyName: string, propertyValue: string) { await find.clickByCssSelector( `[data-test-subj="advancedSetting-editField-${propertyName}"] option[value="${propertyValue}"]` ); @@ -95,7 +99,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } - async setAdvancedSettingsInput(propertyName, propertyValue) { + async setAdvancedSettingsInput(propertyName: string, propertyValue: string) { const input = await testSubjects.find(`advancedSetting-editField-${propertyName}`); await input.clearValue(); await input.type(propertyValue); @@ -103,7 +107,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } - async toggleAdvancedSettingCheckbox(propertyName) { + async toggleAdvancedSettingCheckbox(propertyName: string) { testSubjects.click(`advancedSetting-editField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click(`advancedSetting-saveEditField-${propertyName}`); @@ -126,7 +130,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return await testSubjects.find('createIndexPatternTimeFieldSelect'); } - async selectTimeFieldOption(selection) { + async selectTimeFieldOption(selection: string) { // open dropdown await this.clickTimeFieldNameField(); // close dropdown, keep focus @@ -141,7 +145,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { }); } - async getTimeFieldOption(selection) { + async getTimeFieldOption(selection: string) { return await find.displayedByCssSelector('option[value="' + selection + '"]'); } @@ -174,9 +178,9 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return await find.allByCssSelector('table.euiTable thead tr th'); } - async sortBy(columnName) { + async sortBy(columnName: string) { const chartTypes = await find.allByCssSelector('table.euiTable thead tr th button'); - async function getChartType(chart) { + async function getChartType(chart: Record) { const chartString = await chart.getVisibleText(); if (chartString === columnName) { await chart.click(); @@ -187,7 +191,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return Promise.all(getChartTypesPromises); } - async getTableRow(rowNumber, colNumber) { + async getTableRow(rowNumber: number, colNumber: number) { // passing in zero-based index, but adding 1 for css 1-based indexes return await find.byCssSelector( 'table.euiTable tbody tr:nth-child(' + @@ -234,13 +238,13 @@ export function SettingsPageProvider({ getService, getPageObjects }) { }); } - async setFieldTypeFilter(type) { + async setFieldTypeFilter(type: string) { await find.clickByCssSelector( 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]' ); } - async setScriptedFieldLanguageFilter(language) { + async setScriptedFieldLanguageFilter(language: string) { await find.clickByCssSelector( 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' + language + @@ -248,13 +252,13 @@ export function SettingsPageProvider({ getService, getPageObjects }) { ); } - async filterField(name) { + async filterField(name: string) { const input = await testSubjects.find('indexPatternFieldFilter'); await input.clearValue(); await input.type(name); } - async openControlsByName(name) { + async openControlsByName(name: string) { await this.filterField(name); const tableFields = await ( await find.byCssSelector( @@ -312,7 +316,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { } async createIndexPattern( - indexPatternName, + indexPatternName: string, timefield = '@timestamp', isStandardIndexPattern = true ) { @@ -364,7 +368,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { async getIndexPatternIdFromUrl() { const currentUrl = await browser.getCurrentUrl(); - const indexPatternId = currentUrl.match(/.*\/(.*)/)[1]; + const indexPatternId = currentUrl.match(/.*\/(.*)/)![1]; log.debug('index pattern ID: ', indexPatternId); @@ -423,12 +427,19 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await testSubjects.click('tab-sourceFilters'); } - async editScriptedField(name) { + async editScriptedField(name: string) { await this.filterField(name); await find.clickByCssSelector('.euiTableRowCell--hasActions button:first-child'); } - async addScriptedField(name, language, type, format, popularity, script) { + async addScriptedField( + name: string, + language: string, + type: string, + format: Record, + popularity: string, + script: string + ) { await this.clickAddScriptedField(); await this.setScriptedFieldName(name); if (language) await this.setScriptedFieldLanguage(language); @@ -469,42 +480,42 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } - async setScriptedFieldName(name) { + async setScriptedFieldName(name: string) { log.debug('set scripted field name = ' + name); const field = await testSubjects.find('editorFieldName'); await field.clearValue(); await field.type(name); } - async setScriptedFieldLanguage(language) { + async setScriptedFieldLanguage(language: string) { log.debug('set scripted field language = ' + language); await find.clickByCssSelector( 'select[data-test-subj="editorFieldLang"] > option[value="' + language + '"]' ); } - async setScriptedFieldType(type) { + async setScriptedFieldType(type: string) { log.debug('set scripted field type = ' + type); await find.clickByCssSelector( 'select[data-test-subj="editorFieldType"] > option[value="' + type + '"]' ); } - async setFieldFormat(format) { + async setFieldFormat(format: string) { log.debug('set scripted field format = ' + format); await find.clickByCssSelector( 'select[data-test-subj="editorSelectedFormatId"] > option[value="' + format + '"]' ); } - async setScriptedFieldUrlType(type) { + async setScriptedFieldUrlType(type: string) { log.debug('set scripted field Url type = ' + type); await find.clickByCssSelector( 'select[data-test-subj="urlEditorType"] > option[value="' + type + '"]' ); } - async setScriptedFieldUrlTemplate(template) { + async setScriptedFieldUrlTemplate(template: string) { log.debug('set scripted field Url Template = ' + template); const urlTemplateField = await find.byCssSelector( 'input[data-test-subj="urlEditorUrlTemplate"]' @@ -512,7 +523,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await urlTemplateField.type(template); } - async setScriptedFieldUrlLabelTemplate(labelTemplate) { + async setScriptedFieldUrlLabelTemplate(labelTemplate: string) { log.debug('set scripted field Url Label Template = ' + labelTemplate); const urlEditorLabelTemplate = await find.byCssSelector( 'input[data-test-subj="urlEditorLabelTemplate"]' @@ -520,7 +531,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await urlEditorLabelTemplate.type(labelTemplate); } - async setScriptedFieldDatePattern(datePattern) { + async setScriptedFieldDatePattern(datePattern: string) { log.debug('set scripted field Date Pattern = ' + datePattern); const datePatternField = await find.byCssSelector( 'input[data-test-subj="dateEditorPattern"]' @@ -531,21 +542,21 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await datePatternField.type(datePattern); } - async setScriptedFieldStringTransform(stringTransform) { + async setScriptedFieldStringTransform(stringTransform: string) { log.debug('set scripted field string Transform = ' + stringTransform); await find.clickByCssSelector( 'select[data-test-subj="stringEditorTransform"] > option[value="' + stringTransform + '"]' ); } - async setScriptedFieldPopularity(popularity) { + async setScriptedFieldPopularity(popularity: string) { log.debug('set scripted field popularity = ' + popularity); const field = await testSubjects.find('editorFieldCount'); await field.clearValue(); await field.type(popularity); } - async setScriptedFieldScript(script) { + async setScriptedFieldScript(script: string) { log.debug('set scripted field script = ' + script); const aceEditorCssSelector = '[data-test-subj="editorFieldScript"] .ace_editor'; await find.clickByCssSelector(aceEditorCssSelector); @@ -555,7 +566,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await browser.pressKeys(...script.split('')); } - async openScriptedFieldHelp(activeTab) { + async openScriptedFieldHelp(activeTab: string) { log.debug('open Scripted Fields help'); let isOpen = await testSubjects.exists('scriptedFieldsHelpFlyout'); if (!isOpen) { @@ -577,7 +588,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await flyout.ensureClosed('scriptedFieldsHelpFlyout'); } - async executeScriptedField(script, additionalField) { + async executeScriptedField(script: string, additionalField: string) { log.debug('execute Scripted Fields help'); await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked await this.setScriptedFieldScript(script); @@ -595,7 +606,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return scriptResults; } - async importFile(path, overwriteAll = true) { + async importFile(path: string, overwriteAll = true) { log.debug(`importFile(${path})`); log.debug(`Clicking importObjects`); @@ -645,7 +656,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await testSubjects.click('importSavedObjectsConfirmBtn'); } - async associateIndexPattern(oldIndexPatternId, newIndexPatternTitle) { + async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) { await find.clickByCssSelector( `select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] > [data-test-subj="indexPatternOption-${newIndexPatternTitle}"]` @@ -710,7 +721,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return await deleteButton.isEnabled(); } - async canSavedObjectBeDeleted(id) { + async canSavedObjectBeDeleted(id: string) { const allCheckBoxes = await testSubjects.findAll('checkboxSelectRow*'); for (const checkBox of allCheckBoxes) { if (await checkBox.isSelected()) { @@ -722,6 +733,12 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await checkBox.click(); return await this.canSavedObjectsBeDeleted(); } + + async setNavType(navType: NavSetting) { + await PageObjects.common.navigateToApp('settings'); + await this.clickKibanaSettings(); + await this.setAdvancedSettingsSelect('pageNavigation', navType); + } } return new SettingsPage(); diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index 30e13d551fa28d..1e098e86216e36 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -37,19 +37,19 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP class VisualizeEditorPage { public async clickDataTab() { - await testSubjects.click('visualizeEditDataLink'); + await testSubjects.click('visEditorTab__data'); } public async clickOptionsTab() { - await testSubjects.click('visEditorTaboptions'); + await testSubjects.click('visEditorTab__options'); } public async clickMetricsAndAxes() { - await testSubjects.click('visEditorTabadvanced'); + await testSubjects.click('visEditorTab__advanced'); } public async clickVisEditorTab(tabName: string) { - await testSubjects.click('visEditorTab' + tabName); + await testSubjects.click(`visEditorTab__${tabName}`); await header.waitUntilLoadingHasFinished(); } @@ -134,7 +134,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP public async getBucketErrorMessage() { const error = await find.byCssSelector( - '[group-name="buckets"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText' + '[data-test-subj="bucketsAggGroup"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText' ); const errorMessage = await error.getAttribute('innerText'); log.debug(errorMessage); @@ -152,7 +152,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP ) { log.debug(`selectField ${fieldValue}`); const selector = ` - [group-name="${groupName}"] + [data-test-subj="${groupName}AggGroup"] [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen [data-test-subj="visAggEditorParams"] ${childAggregationType ? '.visEditorAgg__subAgg' : ''} @@ -180,7 +180,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP childAggregationType = false ) { const comboBoxElement = await find.byCssSelector(` - [group-name="${groupName}"] + [data-test-subj="${groupName}AggGroup"] [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen ${childAggregationType ? '.visEditorAgg__subAgg' : ''} [data-test-subj="defaultEditorAggSelect"] @@ -291,8 +291,9 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async sizeUpEditor() { - await testSubjects.click('visualizeEditorResizer'); - await browser.pressKeys(browser.keys.ARROW_RIGHT); + const resizerPanel = await testSubjects.find('splitPanelResizer'); + // Drag panel 100 px left + await browser.dragAndDrop({ location: resizerPanel }, { location: { x: -100, y: 0 } }); } public async toggleDisabledAgg(agg: string) { @@ -320,7 +321,10 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async toggleAutoMode() { - await testSubjects.click('visualizeEditorAutoButton'); + // this is a temporary solution, should be replaced with initial after fixing the EuiToggleButton + // passing the data-test-subj attribute to a checkbox + await find.clickByCssSelector('.visEditorSidebar__controls input[type="checkbox"]'); + // await testSubjects.click('visualizeEditorAutoButton'); } public async isApplyEnabled() { @@ -428,7 +432,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async clickMetricEditor() { - await find.clickByCssSelector('[group-name="metrics"] .euiAccordion__button'); + await find.clickByCssSelector('[data-test-subj="metricsAggGroup"] .euiAccordion__button'); } public async clickMetricByIndex(index: number) { diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 0071b8d993f706..e54e3d1d011540 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -200,7 +200,7 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide } public async getSideEditorExists() { - return await find.existsByCssSelector('.collapsible-sidebar'); + return await find.existsByCssSelector('.visEditor__collapsibleSidebar'); } public async clickSavedSearch(savedSearchName: string) { diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 6a689e85de214c..2d799b7daca732 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -240,8 +240,8 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { * @return {Promise} */ public async dragAndDrop( - from: { offset: { x: any; y: any }; location: any }, - to: { offset: { x: any; y: any }; location: any } + from: { offset?: { x: any; y: any }; location: any }, + to: { offset?: { x: any; y: any }; location: any } ) { if (this.isW3CEnabled) { // The offset should be specified in pixels relative to the center of the element's bounding box diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png similarity index 100% rename from test/interpreter_functional/screenshots/baseline/metric_percentage.png rename to test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json similarity index 100% rename from test/interpreter_functional/snapshots/baseline/metric_percentage.json rename to test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json diff --git a/test/interpreter_functional/snapshots/session/metric_percentage.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json similarity index 100% rename from test/interpreter_functional/snapshots/session/metric_percentage.json rename to test/interpreter_functional/snapshots/session/metric_percentage_mode.json diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.ts b/test/interpreter_functional/test_suites/run_pipeline/metric.ts index c238bedfa28ce1..5f685037d4fad0 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -81,11 +81,15 @@ export default function({ ).toMatchScreenshot(); }); - it('with percentage option', async () => { + it('with percentageMode option', async () => { const expression = - 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; + 'metricVis metric={visdimension 0} percentageMode=true colorRange={range from=0 to=1000}'; await ( - await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot() + await expectExpression( + 'metric_percentage_mode', + expression, + dataContext + ).toMatchSnapshot() ).toMatchScreenshot(); }); }); diff --git a/test/plugin_functional/plugins/core_app_status/public/application.tsx b/test/plugin_functional/plugins/core_app_status/public/application.tsx index 323774392a6d73..b9ebd8d3692f1f 100644 --- a/test/plugin_functional/plugins/core_app_status/public/application.tsx +++ b/test/plugin_functional/plugins/core_app_status/public/application.tsx @@ -31,15 +31,15 @@ import { EuiTitle, } from '@elastic/eui'; -import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { AppMountParameters } from 'kibana/public'; -const AppStatusApp = () => ( +const AppStatusApp = ({ appId }: { appId: string }) => ( -

Welcome to App Status Test App!

+

Welcome to {appId} Test App!

@@ -47,18 +47,18 @@ const AppStatusApp = () => ( -

App Status Test App home page section title

+

{appId} Test App home page section title

- App Status Test App content + {appId} Test App content
); -export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { - render(, element); +export const renderApp = (appId: string, { element }: AppMountParameters) => { + render(, element); return () => unmountComponentAtNode(element); }; diff --git a/test/plugin_functional/plugins/core_app_status/public/index.ts b/test/plugin_functional/plugins/core_app_status/public/index.ts index e0ad7c25a54b81..f52b7ff5fea440 100644 --- a/test/plugin_functional/plugins/core_app_status/public/index.ts +++ b/test/plugin_functional/plugins/core_app_status/public/index.ts @@ -18,7 +18,7 @@ */ import { PluginInitializer } from 'kibana/public'; -import { CoreAppStatusPlugin, CoreAppStatusPluginSetup, CoreAppStatusPluginStart } from './plugin'; +import { CoreAppStatusPlugin, CoreAppStatusPluginStart } from './plugin'; -export const plugin: PluginInitializer = () => +export const plugin: PluginInitializer<{}, CoreAppStatusPluginStart> = () => new CoreAppStatusPlugin(); diff --git a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx index 85caaaf5f9090f..af23bfbe1f8f5f 100644 --- a/test/plugin_functional/plugins/core_app_status/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_app_status/public/plugin.tsx @@ -17,22 +17,38 @@ * under the License. */ -import { Plugin, CoreSetup, AppUpdater, AppUpdatableFields, CoreStart } from 'kibana/public'; import { BehaviorSubject } from 'rxjs'; +import { + Plugin, + CoreSetup, + AppUpdater, + AppUpdatableFields, + CoreStart, + AppMountParameters, +} from 'kibana/public'; +import './types'; -export class CoreAppStatusPlugin - implements Plugin { +export class CoreAppStatusPlugin implements Plugin<{}, CoreAppStatusPluginStart> { private appUpdater = new BehaviorSubject(() => ({})); public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'app_status_start', + title: 'App Status Start Page', + async mount(params: AppMountParameters) { + const { renderApp } = await import('./application'); + return renderApp('app_status_start', params); + }, + }); + core.application.register({ id: 'app_status', title: 'App Status', euiIconType: 'snowflake', updater$: this.appUpdater, - async mount(context, params) { + async mount(params: AppMountParameters) { const { renderApp } = await import('./application'); - return renderApp(context, params); + return renderApp('app_status', params); }, }); @@ -40,7 +56,7 @@ export class CoreAppStatusPlugin } public start(core: CoreStart) { - return { + const startContract = { setAppStatus: (status: Partial) => { this.appUpdater.next(() => status); }, @@ -48,9 +64,10 @@ export class CoreAppStatusPlugin return core.application.navigateToApp(appId); }, }; + window.__coreAppStatus = startContract; + return startContract; } public stop() {} } -export type CoreAppStatusPluginSetup = ReturnType; export type CoreAppStatusPluginStart = ReturnType; diff --git a/src/legacy/server/url_shortening/url_shortening_mixin.js b/test/plugin_functional/plugins/core_app_status/public/types.ts similarity index 84% rename from src/legacy/server/url_shortening/url_shortening_mixin.js rename to test/plugin_functional/plugins/core_app_status/public/types.ts index 867898cac845a3..7c708e6c26d91f 100644 --- a/src/legacy/server/url_shortening/url_shortening_mixin.js +++ b/test/plugin_functional/plugins/core_app_status/public/types.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { createRoutes } from './routes/create_routes'; -export function urlShorteningMixin(kbnServer, server) { - createRoutes(server); +import { CoreAppStatusPluginStart } from './plugin'; + +declare global { + interface Window { + __coreAppStatus: CoreAppStatusPluginStart; + } } diff --git a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx index 8b7cdd653ed8c3..f3b7a19f70ae3b 100644 --- a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx @@ -62,6 +62,22 @@ export class ManagementTestPlugin }; }, }); + + testSection! + .registerApp({ + id: 'test-management-disabled', + title: 'Management Test Disabled', + mount(params) { + params.setBreadcrumbs([{ text: 'Management Test Disabled' }]); + ReactDOM.render(
This is a secret that should never be seen!
, params.element); + + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }) + .disable(); + return {}; } diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index 703ae30533bae0..0cc64277efe11b 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -24,50 +24,36 @@ import { AppUpdatableFields, } from '../../../../src/core/public/application/types'; import { PluginFunctionalProviderContext } from '../../services'; -import { CoreAppStatusPluginStart } from '../../plugins/core_app_status/public/plugin'; -import '../../plugins/core_provider_plugin/types'; +import '../../plugins/core_app_status/public/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'settings']); const browser = getService('browser'); const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); const setAppStatus = async (s: Partial) => { - await browser.executeAsync(async (status: Partial, cb: Function) => { - const plugin = window.__coreProvider.start.plugins - .core_app_status as CoreAppStatusPluginStart; - plugin.setAppStatus(status); + return browser.executeAsync(async (status: Partial, cb: Function) => { + window.__coreAppStatus.setAppStatus(status); cb(); }, s); }; - const navigateToApp = async (i: string): Promise<{ error?: string }> => { + const navigateToApp = async (i: string) => { return (await browser.executeAsync(async (appId, cb: Function) => { - // navigating in legacy mode performs a page refresh - // and webdriver seems to re-execute the script after the reload - // as it considers it didn't end on the previous session. - // however when testing navigation to NP app, __coreProvider is not accessible - // so we need to check for existence. - if (!window.__coreProvider) { - cb({}); - } - const plugin = window.__coreProvider.start.plugins - .core_app_status as CoreAppStatusPluginStart; - try { - await plugin.navigateToApp(appId); - cb({}); - } catch (e) { - cb({ - error: e.message, - }); - } + await window.__coreAppStatus.navigateToApp(appId); + cb(); }, i)) as any; }; describe('application status management', () => { + before(async () => { + await PageObjects.settings.setNavType('individual'); + }); + beforeEach(async () => { - await PageObjects.common.navigateToApp('settings'); + await PageObjects.common.navigateToApp('app_status_start'); }); it('can change the navLink status at runtime', async () => { @@ -98,10 +84,10 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider status: AppStatus.inaccessible, }); - const result = await navigateToApp('app_status'); - expect(result.error).to.contain( - 'Trying to navigate to an inaccessible application: app_status' - ); + await navigateToApp('app_status'); + + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(true); + expect(await testSubjects.exists('appStatusApp')).to.eql(false); }); it('allows to navigate to an accessible app', async () => { @@ -109,8 +95,35 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider status: AppStatus.accessible, }); - const result = await navigateToApp('app_status'); - expect(result.error).to.eql(undefined); + await navigateToApp('app_status'); + + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(false); + expect(await testSubjects.exists('appStatusApp')).to.eql(true); + }); + + it('can change the state of the currently mounted app', async () => { + await setAppStatus({ + status: AppStatus.accessible, + }); + + await navigateToApp('app_status'); + + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(false); + expect(await testSubjects.exists('appStatusApp')).to.eql(true); + + await setAppStatus({ + status: AppStatus.inaccessible, + }); + + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(true); + expect(await testSubjects.exists('appStatusApp')).to.eql(false); + + await setAppStatus({ + status: AppStatus.accessible, + }); + + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(false); + expect(await testSubjects.exists('appStatusApp')).to.eql(true); }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index f50d4605325560..6567837f653095 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -122,7 +122,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('can navigate from NP apps to legacy apps', async () => { - await appsMenu.clickLink('Management'); + await appsMenu.clickLink('Stack Management'); await loadingScreenShown(); await testSubjects.existOrFail('managementNav'); }); diff --git a/test/plugin_functional/test_suites/management/management_plugin.js b/test/plugin_functional/test_suites/management/management_plugin.js index d65fb1dcd3a7e3..0c185f4b385b51 100644 --- a/test/plugin_functional/test_suites/management/management_plugin.js +++ b/test/plugin_functional/test_suites/management/management_plugin.js @@ -36,5 +36,13 @@ export default function({ getService, getPageObjects }) { await testSubjects.click('test-management-link-basepath'); await testSubjects.existOrFail('test-management-link-one'); }); + + it('should redirect when app is disabled', async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + 'management/test-section/test-management-disabled' + ); + await testSubjects.existOrFail('management-landing'); + }); }); } diff --git a/tsconfig.json b/tsconfig.json index a2da9c127e7ba6..811b05abeb648c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ ], "test_utils/*": [ "src/test_utils/public/*" - ] + ], + "fixtures/*": ["src/fixtures/*"] }, // Support .tsx files and transform JSX into calls to React.createElement "jsx": "react", @@ -51,7 +52,8 @@ "types": [ "node", "jest", - "react" + "react", + "flot" ] }, "include": [ diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index f38181ce56a2f3..b746f0ae258cd1 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -17,6 +17,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], moduleNameMapper: { '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, + '^fixtures/(.*)': `${kibanaDirectory}/src/fixtures/$1`, 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 2af66059d9fed2..9b8cfe9b69ed04 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -883,7 +883,6 @@ describe('enable()', () => { schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, - scheduledTaskId: 'task-123', updatedBy: 'elastic', apiKey: null, apiKeyOwner: null, @@ -892,6 +891,9 @@ describe('enable()', () => { version: '123', } ); + expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + scheduledTaskId: 'task-123', + }); expect(taskManager.schedule).toHaveBeenCalledWith({ taskType: `alerting:2`, params: { @@ -964,7 +966,6 @@ describe('enable()', () => { schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, - scheduledTaskId: 'task-123', apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', @@ -973,6 +974,9 @@ describe('enable()', () => { version: '123', } ); + expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + scheduledTaskId: 'task-123', + }); expect(taskManager.schedule).toHaveBeenCalledWith({ taskType: `alerting:2`, params: { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index fe96a233b8663f..7801e8f478712e 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -362,7 +362,6 @@ export class AlertsClient { }); if (attributes.enabled === false) { - const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); const username = await this.getUserName(); await this.savedObjectsClient.update( 'alert', @@ -372,11 +371,11 @@ export class AlertsClient { enabled: true, ...this.apiKeyAsAlertAttributes(await this.createAPIKey(), username), updatedBy: username, - - scheduledTaskId: scheduledTask.id, }, { version } ); + const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); + await this.savedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id }); await this.invalidateApiKey({ apiKey: attributes.apiKey }); } } diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 0934cb0019f44c..c52e6742ddae58 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -9,6 +9,7 @@ import { Server } from 'hapi'; import { resolve } from 'path'; import { APMPluginContract } from '../../../plugins/apm/server'; import { LegacyPluginInitializer } from '../../../../src/legacy/types'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; import mappings from './mappings.json'; import { makeApmUsageCollector } from './server/lib/apm_telemetry'; @@ -18,7 +19,6 @@ export const apm: LegacyPluginInitializer = kibana => { id: 'apm', configPrefix: 'xpack.apm', publicDir: resolve(__dirname, 'public'), - uiExports: { app: { title: 'APM', @@ -28,7 +28,8 @@ export const apm: LegacyPluginInitializer = kibana => { main: 'plugins/apm/index', icon: 'plugins/apm/icon.svg', euiIconType: 'apmApp', - order: 8100 + order: 8100, + category: DEFAULT_APP_CATEGORIES.observability }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), home: ['plugins/apm/legacy_register_feature'], diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 67bff86c8ccdfc..32432b7b85ef60 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -18,8 +18,7 @@ import { history } from '../../../utils/history'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { - AutocompleteProvider, - AutocompleteSuggestion, + autocomplete, esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; @@ -29,7 +28,7 @@ const Container = styled.div` `; interface State { - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; isLoadingSuggestions: boolean; } @@ -38,32 +37,6 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { return esKuery.toElasticsearchQuery(ast, indexPattern); } -function getSuggestions( - query: string, - selectionStart: number, - indexPattern: IIndexPattern, - boolFilter: unknown, - autocompleteProvider?: AutocompleteProvider -) { - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [indexPattern], - boolFilter - }); - return getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd: selectionStart - }); -} - export function KueryBar() { const [state, setState] = useState({ suggestions: [], @@ -72,7 +45,6 @@ export function KueryBar() { const { urlParams } = useUrlParams(); const location = useLocation(); const { data } = useApmPluginContext().plugins; - const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; @@ -100,16 +72,16 @@ export function KueryBar() { const currentRequest = uniqueId(); currentRequestCheck = currentRequest; - const boolFilter = getBoolFilter(urlParams); try { const suggestions = ( - await getSuggestions( - inputValue, + (await data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: getBoolFilter(urlParams), + query: inputValue, selectionStart, - indexPattern, - boolFilter, - autocompleteProvider - ) + selectionEnd: selectionStart + })) || [] ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) .slice(0, 15); diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx index 1f49711ae5e4a1..f3e0f3dfbdae7d 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx @@ -13,7 +13,7 @@ import { import React from 'react'; import styled from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import { composeStateUpdaters } from '../../utils/typed_react'; import { SuggestionItem } from './suggestion_item'; @@ -25,7 +25,7 @@ interface AutocompleteFieldProps { onSubmit?: (value: string) => void; onChange?: (value: string) => void; placeholder?: string; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; } diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx index a753a944a2ecb2..0132667b9e510b 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx @@ -9,13 +9,13 @@ import { tint } from 'polished'; import React from 'react'; import styled from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; } export const SuggestionItem: React.FC = props => { diff --git a/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx b/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx index 26ddd682405cbb..d1cbc0888dca8c 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx @@ -8,7 +8,7 @@ import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eu import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import { TABLE_CONFIG } from '../../../common/constants'; import { AutocompleteField } from '../autocomplete_field/index'; import { ControlSchema } from './action_schema'; @@ -31,7 +31,7 @@ export interface KueryBarProps { loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; onChange?: (value: string) => void; onSubmit?: (value: string) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; } diff --git a/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx b/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx index 2ac20438536c8a..db73a7cb38c110 100644 --- a/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/legacy/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; import { FrontendLibs } from '../lib/types'; import { RendererFunction } from '../utils/typed_react'; @@ -17,7 +17,7 @@ interface WithKueryAutocompletionLifecycleProps { children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; }>; } @@ -28,7 +28,7 @@ interface WithKueryAutocompletionLifecycleState { expression: string; cursorPosition: number; } | null; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; } export class WithKueryAutocompletion extends React.Component< diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts index 4f4ce70e817c6d..12898027d5fb57 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../../src/plugins/data/public'; export interface ElasticsearchAdapter { convertKueryToEsQuery: (kuery: string) => Promise; - getSuggestions: (kuery: string, selectionStart: any) => Promise; + getSuggestions: (kuery: string, selectionStart: any) => Promise; isKueryValid(kuery: string): boolean; } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts index e001bf6c6e8442..111255b55c99ba 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../../src/plugins/data/public'; import { ElasticsearchAdapter } from './adapter_types'; export class MemoryElasticsearchAdapter implements ElasticsearchAdapter { constructor( private readonly mockIsKueryValid: (kuery: string) => boolean, private readonly mockKueryToEsQuery: (kuery: string) => string, - private readonly suggestions: AutocompleteSuggestion[] + private readonly suggestions: autocomplete.QuerySuggestion[] ) {} public isKueryValid(kuery: string): boolean { @@ -23,7 +23,7 @@ export class MemoryElasticsearchAdapter implements ElasticsearchAdapter { public async getSuggestions( kuery: string, selectionStart: any - ): Promise { + ): Promise { return this.suggestions; } } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 8771181639f4d3..fc400c600e5758 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -7,10 +7,7 @@ import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; -import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; - -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); +import { autocomplete, esKuery } from '../../../../../../../../src/plugins/data/public'; export class RestElasticsearchAdapter implements ElasticsearchAdapter { private cachedIndexPattern: any = null; @@ -33,30 +30,23 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { const indexPattern = await this.getIndexPattern(); return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); } + public async getSuggestions( kuery: string, selectionStart: any - ): Promise { - const autocompleteProvider = getAutocompleteProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true, - }; + ): Promise { const indexPattern = await this.getIndexPattern(); - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [indexPattern], - boolFilter: null, - }); - const results = getAutocompleteSuggestions({ - query: kuery || '', - selectionStart, - selectionEnd: selectionStart, - }); - return results; + return ( + (await npStart.plugins.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: kuery || '', + selectionStart, + selectionEnd: selectionStart, + })) || [] + ); } private async getIndexPattern() { diff --git a/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts b/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts index f357e698afc3aa..47df51dea8620b 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/compose/memory.ts @@ -24,14 +24,14 @@ import { TagsLib } from '../tags'; import { FrontendLibs } from '../types'; import { MemoryElasticsearchAdapter } from './../adapters/elasticsearch/memory'; import { ElasticsearchLib } from './../elasticsearch'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; const onKibanaReady = uiModules.get('kibana').run; export function compose( mockIsKueryValid: (kuery: string) => boolean, mockKueryToEsQuery: (kuery: string) => string, - suggestions: AutocompleteSuggestion[] + suggestions: autocomplete.QuerySuggestion[] ): FrontendLibs { const esAdapter = new MemoryElasticsearchAdapter( mockIsKueryValid, diff --git a/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts b/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts index 0897dfd9c13928..d71512e80d3d55 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/elasticsearch.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AutocompleteSuggestion } from '../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../src/plugins/data/public'; import { ElasticsearchAdapter } from './adapters/elasticsearch/adapter_types'; interface HiddenFields { @@ -35,7 +35,7 @@ export class ElasticsearchLib { kuery: string, selectionStart: any, fieldPrefix?: string - ): Promise { + ): Promise { const suggestions = await this.adapter.getSuggestions(kuery, selectionStart); const filteredSuggestions = suggestions.filter(suggestion => { diff --git a/x-pack/legacy/plugins/canvas/index.js b/x-pack/legacy/plugins/canvas/index.js index 8e742de6de9448..ebd4f35db8175c 100644 --- a/x-pack/legacy/plugins/canvas/index.js +++ b/x-pack/legacy/plugins/canvas/index.js @@ -5,6 +5,7 @@ */ import { resolve } from 'path'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; import { init } from './init'; import { mappings } from './server/mappings'; import { CANVAS_APP, CANVAS_TYPE, CUSTOM_ELEMENT_TYPE } from './common/lib'; @@ -23,6 +24,7 @@ export function canvas(kibana) { icon: 'plugins/canvas/icon.svg', euiIconType: 'canvasApp', main: 'plugins/canvas/legacy_start', + category: DEFAULT_APP_CATEGORIES.analyze, }, interpreter: [ 'plugins/canvas/browser_functions', diff --git a/x-pack/legacy/plugins/dashboard_mode/index.js b/x-pack/legacy/plugins/dashboard_mode/index.js index 4a042498443220..94655adf981b48 100644 --- a/x-pack/legacy/plugins/dashboard_mode/index.js +++ b/x-pack/legacy/plugins/dashboard_mode/index.js @@ -5,15 +5,13 @@ */ import { resolve } from 'path'; - +import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; import { CONFIG_DASHBOARD_ONLY_MODE_ROLES } from './common'; - import { createDashboardModeRequestInterceptor } from './server'; -import { i18n } from '@kbn/i18n'; - // Copied largely from plugins/kibana/index.js. The dashboard viewer includes just the dashboard section of -// the standard kibana plugin. We don't want to include code for the other links (visualize, dev tools, etc) +// the standard kibana plugin. We don't want to include code for the other links (visualize, dev tools, etc) // since it's view only, but we want the urls to be the same, so we are using largely the same setup. export function dashboardMode(kibana) { const kbnBaseUrl = '/app/kibana'; @@ -64,6 +62,7 @@ export function dashboardMode(kibana) { } ), icon: 'plugins/kibana/dashboard/assets/dashboard.svg', + category: DEFAULT_APP_CATEGORIES.analyze, }, ], }, diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 391973f6d909b4..fbf917054edbff 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -34,6 +34,7 @@ import 'ui/color_maps'; import 'ui/agg_response'; import 'ui/agg_types'; import 'leaflet'; +import 'plugins/kibana/dashboard/legacy'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; diff --git a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js index f1e74919d734b3..0ee4f76ebf9d0c 100644 --- a/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js +++ b/x-pack/legacy/plugins/file_upload/public/components/json_index_file_picker.js @@ -13,6 +13,8 @@ import { MAX_FILE_SIZE } from '../../common/constants/file_import'; import _ from 'lodash'; const ACCEPTABLE_FILETYPES = ['json', 'geojson']; +const acceptedFileTypeString = ACCEPTABLE_FILETYPES.map(type => `.${type}`).join(','); +const acceptedFileTypeStringMessage = ACCEPTABLE_FILETYPES.map(type => `.${type}`).join(', '); export class JsonIndexFilePicker extends Component { state = { @@ -103,6 +105,7 @@ export class JsonIndexFilePicker extends Component { const splitNameArr = name.split('.'); const fileType = splitNameArr.pop(); if (!ACCEPTABLE_FILETYPES.includes(fileType)) { + //should only occur if browser does not accept the accept parameter throw new Error( i18n.translate('xpack.fileUpload.jsonIndexFilePicker.acceptableTypesError', { defaultMessage: 'File is not one of acceptable types: {types}', @@ -252,7 +255,10 @@ export class JsonIndexFilePicker extends Component { ) : ( {i18n.translate('xpack.fileUpload.jsonIndexFilePicker.formatsAccepted', { - defaultMessage: 'Formats accepted: .json, .geojson', + defaultMessage: 'Formats accepted: {acceptedFileTypeStringMessage}', + values: { + acceptedFileTypeStringMessage, + }, })}{' '}
} onChange={this._fileHandler} + accept={acceptedFileTypeString} /> diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index 601a239574e6b5..f798fa5e9f39d4 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import migrations from './migrations'; import mappings from './mappings.json'; import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export const graph: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ @@ -25,6 +26,7 @@ export const graph: LegacyPluginInitializer = kibana => { icon: 'plugins/graph/icon.png', euiIconType: 'graphApp', main: 'plugins/graph/index', + category: DEFAULT_APP_CATEGORIES.analyze, }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, diff --git a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index f34b82d6bb1a3c..d1fcbea2ff5b72 100644 --- a/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/legacy/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -146,7 +146,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) { ); if (noIndexPatterns) { - const managementUrl = chrome.navLinks.get('kibana:management')!.url; + const managementUrl = chrome.navLinks.get('kibana:stack_management')!.url; const indexPatternUrl = `${managementUrl}/kibana/index_patterns`; const sampleDataUrl = `${application.getUrlForApp( 'kibana' diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index 360561df719571..95b7dd22e9fcf4 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -21,6 +21,7 @@ import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; +jest.mock('ui/new_platform'); jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise(r => setTimeout(r)); @@ -51,7 +52,7 @@ function wrapSearchBarInContext(testProps: OuterSearchBarProps) { savedQueries: {}, }, autocomplete: { - getProvider: () => undefined, + hasQuerySuggestions: () => false, }, }, }; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index d1b0ee3a3e83e3..f79741d9a1a9f1 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -14,9 +14,6 @@ import { UseField, FormDataProvider, FormRow, ToggleField } from '../../../share import { ComboBoxOption } from '../../../types'; export const SourceFieldSection = () => { - const [includeComboBoxOptions, setIncludeComboBoxOptions] = useState([]); - const [excludeComboBoxOptions, setExcludeComboBoxOptions] = useState([]); - const renderWarning = () => ( { {({ label, helpText, value, setValue }) => ( { setValue(newValue); @@ -90,7 +87,6 @@ export const SourceFieldSection = () => { }; setValue([...(value as ComboBoxOption[]), newOption]); - setIncludeComboBoxOptions([...includeComboBoxOptions, newOption]); }} fullWidth /> @@ -104,13 +100,13 @@ export const SourceFieldSection = () => { {({ label, helpText, value, setValue }) => ( { setValue(newValue); @@ -121,7 +117,6 @@ export const SourceFieldSection = () => { }; setValue([...(value as ComboBoxOption[]), newOption]); - setExcludeComboBoxOptions([...excludeComboBoxOptions, newOption]); }} fullWidth /> diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts index 9622466ad795cf..b248776c884f12 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -51,3 +51,5 @@ export * from './fielddata_parameter'; export * from './split_queries_on_whitespace_parameter'; export * from './locale_parameter'; + +export * from './max_shingle_size_parameter'; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx new file mode 100644 index 00000000000000..bc1917b2da966b --- /dev/null +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/field_parameters/max_shingle_size_parameter.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { getFieldConfig } from '../../../lib'; +import { EditFieldFormRow } from '../fields/edit_field'; +import { UseField, Field } from '../../../shared_imports'; + +interface Props { + defaultToggleValue: boolean; +} + +export const MaxShingleSizeParameter = ({ defaultToggleValue }: Props) => ( + + + +); diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx index 83541ec982ee68..dafbebd24b3fa7 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx @@ -14,6 +14,7 @@ import { NormsParameter, SimilarityParameter, TermVectorParameter, + MaxShingleSizeParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; @@ -24,7 +25,8 @@ interface Props { const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'similarity': - case 'term_vector': { + case 'term_vector': + case 'max_shingle_size': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } case 'analyzers': { @@ -47,6 +49,10 @@ export const SearchAsYouType = React.memo(({ field }: Props) => { + + { coerce: 1234, coerce_shape: '', ignore_malformed: 0, - null_value: {}, null_value_numeric: 'abc', null_value_boolean: [], copy_to: [], @@ -257,6 +256,7 @@ describe('Properties validator', () => { enable_position_increments: [], depth_limit: true, dims: false, + max_shingle_size: 'string_not_allowed', }, // All the parameters in "goodField" have the correct format // and should still be there after the validation ran. @@ -308,6 +308,7 @@ describe('Properties validator', () => { enable_position_increments: true, depth_limit: 20, dims: 'abc', + max_shingle_size: 2, }, }; diff --git a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts index 0fce3422344bc2..b7bf4e6b112d37 100644 --- a/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts +++ b/x-pack/legacy/plugins/index_management/public/app/components/mappings_editor/types.ts @@ -119,7 +119,8 @@ export type ParameterName = | 'points_only' | 'path' | 'dims' - | 'depth_limit'; + | 'depth_limit' + | 'max_shingle_size'; export interface Parameter { fieldConfig: FieldConfig; diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index 196950b51be3ab..d9abadcb5125c5 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -18,6 +18,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/fea import { SpacesPluginSetup } from '../../../plugins/spaces/server'; import { VisTypeTimeseriesSetup } from '../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginContract } from '../../../plugins/apm/server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export const APP_ID = 'infra'; @@ -55,6 +56,7 @@ export function infra(kibana: any) { defaultMessage: 'Metrics', }), url: `/app/${APP_ID}#/infrastructure`, + category: DEFAULT_APP_CATEGORIES.observability, }, { description: i18n.translate('xpack.infra.linkLogsDescription', { @@ -68,6 +70,7 @@ export function infra(kibana: any) { defaultMessage: 'Logs', }), url: `/app/${APP_ID}#/logs`, + category: DEFAULT_APP_CATEGORIES.observability, }, ], mappings: savedObjectMappings, diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 4c215835ca2404..dc6eabb325d16f 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import euiStyled from '../../../../../common/eui_styled_components'; import { composeStateUpdaters } from '../../utils/typed_react'; @@ -25,7 +25,7 @@ interface AutocompleteFieldProps { onSubmit?: (value: string) => void; onChange?: (value: string) => void; placeholder?: string; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; autoFocus?: boolean; 'aria-label'?: string; diff --git a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx index 0c29b1f51b07e2..79b18f5888bd57 100644 --- a/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx @@ -8,14 +8,14 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import euiStyled from '../../../../../common/eui_styled_components'; interface Props { isSelected?: boolean; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; } export const SuggestionItem: React.FC = props => { diff --git a/x-pack/legacy/plugins/infra/public/components/fixed_datepicker.tsx b/x-pack/legacy/plugins/infra/public/components/fixed_datepicker.tsx new file mode 100644 index 00000000000000..aab1bcd1da8730 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/fixed_datepicker.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiDatePicker, EuiDatePickerProps } from '@elastic/eui'; +import euiStyled from '../../../../common/eui_styled_components'; + +export const FixedDatePicker = euiStyled( + ({ + className, + inputClassName, + ...datePickerProps + }: { + className?: string; + inputClassName?: string; + } & EuiDatePickerProps) => ( + + ) +)` + z-index: 3 !important; +`; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx index 4319f844b1dcc8..02119fd1c09dd2 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_timerange_form.tsx @@ -5,8 +5,6 @@ */ import { - EuiDatePicker, - EuiDatePickerProps, EuiDescribedFormGroup, EuiFlexGroup, EuiFormControlLayout, @@ -16,8 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React, { useMemo } from 'react'; - -import { euiStyled } from '../../../../../../../common/eui_styled_components'; +import { FixedDatePicker } from '../../../fixed_datepicker'; const startTimeLabel = i18n.translate('xpack.infra.analysisSetup.startTimeLabel', { defaultMessage: 'Start time', @@ -138,18 +135,3 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ ); }; - -const FixedDatePicker = euiStyled( - ({ - className, - inputClassName, - ...datePickerProps - }: { - className?: string; - inputClassName?: string; - } & EuiDatePickerProps) => ( - - ) -)` - z-index: 3 !important; -`; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx index 8f5705a9b9c563..5095edd4c715c5 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_time_controls.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React from 'react'; +import { FixedDatePicker } from '../fixed_datepicker'; const noop = () => undefined; @@ -56,7 +57,7 @@ export class LogTimeControls extends React.PureComponent { return ( - - npStart.plugins.data.autocomplete.getProvider(language); - interface WithKueryAutocompletionLifecycleProps { children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; }>; indexPattern: IIndexPattern; } @@ -28,7 +25,7 @@ interface WithKueryAutocompletionLifecycleState { expression: string; cursorPosition: number; } | null; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; } export class WithKueryAutocompletion extends React.Component< @@ -56,21 +53,13 @@ export class WithKueryAutocompletion extends React.Component< maxSuggestions?: number ) => { const { indexPattern } = this.props; - const autocompletionProvider = getAutocompleteProvider('kuery'); - const config = { - get: () => true, - }; + const language = 'kuery'; + const hasQuerySuggestions = npStart.plugins.data.autocomplete.hasQuerySuggestions(language); - if (!autocompletionProvider) { + if (!hasQuerySuggestions) { return; } - const getSuggestions = autocompletionProvider({ - config, - indexPatterns: [indexPattern], - boolFilter: [], - }); - this.setState({ currentRequest: { expression, @@ -79,11 +68,15 @@ export class WithKueryAutocompletion extends React.Component< suggestions: [], }); - const suggestions = await getSuggestions({ - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - }); + const suggestions = + (await npStart.plugins.data.autocomplete.getQuerySuggestions({ + language, + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + indexPatterns: [indexPattern], + boolFilter: [], + })) || []; this.setState(state => state.currentRequest && diff --git a/x-pack/legacy/plugins/infra/server/features.ts b/x-pack/legacy/plugins/infra/server/features.ts index fc20813c777b69..02658002694d27 100644 --- a/x-pack/legacy/plugins/infra/server/features.ts +++ b/x-pack/legacy/plugins/infra/server/features.ts @@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n'; export const METRICS_FEATURE = { id: 'infrastructure', name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { - defaultMessage: 'Infrastructure', + defaultMessage: 'Metrics', }), - icon: 'infraApp', + icon: 'metricsApp', navLinkId: 'infra:home', app: ['infra', 'kibana'], catalogue: ['infraops'], @@ -40,7 +40,7 @@ export const LOGS_FEATURE = { name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { defaultMessage: 'Logs', }), - icon: 'loggingApp', + icon: 'logsApp', navLinkId: 'infra:logs', app: ['infra', 'kibana'], catalogue: ['infralogging'], diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js deleted file mode 100644 index a5fb4eff388f72..00000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { flatten, mapValues, uniq } from 'lodash'; -import { getSuggestionsProvider as field } from './field'; -import { getSuggestionsProvider as value } from './value'; -import { getSuggestionsProvider as operator } from './operator'; -import { getSuggestionsProvider as conjunction } from './conjunction'; -import { esKuery } from '../../../../../../src/plugins/data/public'; - -const cursorSymbol = '@kuery-cursor@'; - -function dedup(suggestions) { - return uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); -} - -export const kueryProvider = ({ config, indexPatterns, boolFilter }) => { - const getSuggestionsByType = mapValues({ field, value, operator, conjunction }, provider => { - return provider({ config, indexPatterns, boolFilter }); - }); - - return function getSuggestions({ query, selectionStart, selectionEnd, signal }) { - const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr( - selectionEnd - )}`; - - let cursorNode; - try { - cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); - } catch (e) { - cursorNode = {}; - } - - const { suggestionTypes = [] } = cursorNode; - const suggestionsByType = suggestionTypes.map(type => { - return getSuggestionsByType[type](cursorNode, signal); - }); - return Promise.all(suggestionsByType).then(suggestionsByType => - dedup(flatten(suggestionsByType)) - ); - }; -}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__fixtures__/index_pattern_response.json b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__fixtures__/index_pattern_response.json similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__fixtures__/index_pattern_response.json rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__fixtures__/index_pattern_response.json diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/conjunction.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/conjunction.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/escape_kuery.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/escape_kuery.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/field.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/field.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/operator.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__tests__/operator.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/conjunction.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/conjunction.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/escape_kuery.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/escape_kuery.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/escape_kuery.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/field.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js new file mode 100644 index 00000000000000..b877f9eb852d5d --- /dev/null +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.js @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { flatten, uniq } from 'lodash'; +import { getSuggestionsProvider as field } from './field'; +import { getSuggestionsProvider as value } from './value'; +import { getSuggestionsProvider as operator } from './operator'; +import { getSuggestionsProvider as conjunction } from './conjunction'; +import { esKuery } from '../../../../../../src/plugins/data/public'; + +const cursorSymbol = '@kuery-cursor@'; +const providers = { + field, + value, + operator, + conjunction, +}; + +function dedup(suggestions) { + return uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); +} + +const getProviderByType = (type, args) => providers[type](args); + +export const setupKqlQuerySuggestionProvider = ({ uiSettings }) => ({ + indexPatterns, + boolFilter, + query, + selectionStart, + selectionEnd, + signal, +}) => { + const cursoredQuery = `${query.substr(0, selectionStart)}${cursorSymbol}${query.substr( + selectionEnd + )}`; + + let cursorNode; + try { + cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); + } catch (e) { + cursorNode = {}; + } + + const { suggestionTypes = [] } = cursorNode; + const suggestionsByType = suggestionTypes.map(type => + getProviderByType(type, { + config: uiSettings, + indexPatterns, + boolFilter, + })(cursorNode, signal) + ); + return Promise.all(suggestionsByType).then(suggestionsByType => + dedup(flatten(suggestionsByType)) + ); +}; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.js diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.test.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.test.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.test.ts rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.test.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/sort_prefix_first.ts rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js similarity index 71% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js index f44a3d9d658f3b..9d0d70fd95747a 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.js @@ -15,7 +15,7 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) { indexPatterns.map(indexPattern => { return indexPattern.fields.map(field => ({ ...field, - indexPatternTitle: indexPattern.title, + indexPattern, })); }) ); @@ -27,18 +27,22 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) { const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName; const fields = allFields.filter(field => field.name === fullFieldName); const query = `${prefix}${suffix}`.trim(); - const { getSuggestions } = npStart.plugins.data; + const { getValueSuggestions } = npStart.plugins.data.autocomplete; - const suggestionsByField = fields.map(field => { - return getSuggestions(field.indexPatternTitle, field, query, boolFilter, signal).then( - data => { - const quotedValues = data.map(value => - typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}` - ); - return wrapAsSuggestions(start, end, query, quotedValues); - } - ); - }); + const suggestionsByField = fields.map(field => + getValueSuggestions({ + indexPattern: field.indexPattern, + field, + query, + boolFilter, + signal, + }).then(data => { + const quotedValues = data.map(value => + typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}` + ); + return wrapAsSuggestions(start, end, query, quotedValues); + }) + ); return Promise.all(suggestionsByField).then(suggestions => flatten(suggestions)); }; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.test.js b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js similarity index 55% rename from x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.test.js rename to x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js index d989fd9046a4d5..f5b652d2e21641 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/value.test.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.js @@ -6,25 +6,26 @@ import { getSuggestionsProvider } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; - import { npStart } from 'ui/new_platform'; jest.mock('ui/new_platform', () => ({ npStart: { plugins: { data: { - getSuggestions: (_, field) => { - let res; - if (field.type === 'boolean') { - res = [true, false]; - } else if (field.name === 'machine.os') { - res = ['Windo"ws', "Mac'", 'Linux']; - } else if (field.name === 'nestedField.child') { - res = ['foo']; - } else { - res = []; - } - return Promise.resolve(res); + autocomplete: { + getValueSuggestions: jest.fn(({ field }) => { + let res; + if (field.type === 'boolean') { + res = [true, false]; + } else if (field.name === 'machine.os') { + res = ['Windo"ws', "Mac'", 'Linux']; + } else if (field.name === 'nestedField.child') { + res = ['foo']; + } else { + res = []; + } + return Promise.resolve(res); + }), }, }, }, @@ -49,19 +50,24 @@ describe('Kuery value suggestions', function() { const fieldName = 'i_dont_exist'; const prefix = ''; const suffix = ''; - const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions'); + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); expect(suggestions.map(({ text }) => text)).toEqual([]); - expect(spy).toHaveBeenCalledTimes(0); + + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(0); }); test('should format suggestions', async () => { - const fieldName = 'ssl'; // Has results with quotes in mock - const prefix = ''; - const suffix = ''; const start = 1; const end = 5; - const suggestions = await getSuggestions({ fieldName, prefix, suffix, start, end }); + const suggestions = await getSuggestions({ + fieldName: 'ssl', + prefix: '', + suffix: '', + start, + end, + }); + expect(suggestions[0].type).toEqual('value'); expect(suggestions[0].start).toEqual(start); expect(suggestions[0].end).toEqual(end); @@ -80,64 +86,60 @@ describe('Kuery value suggestions', function() { describe('Boolean suggestions', function() { test('should stringify boolean fields', async () => { - const fieldName = 'ssl'; - const prefix = ''; - const suffix = ''; - const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions'); - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: '', suffix: '' }); + expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']); - expect(spy).toHaveBeenCalledTimes(1); + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1); }); test('should filter out boolean suggestions', async () => { - const fieldName = 'ssl'; // Has results with quotes in mock - const prefix = 'fa'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ fieldName: 'ssl', prefix: 'fa', suffix: '' }); + expect(suggestions.length).toEqual(1); }); }); describe('String suggestions', function() { test('should merge prefix and suffix', async () => { - const fieldName = 'machine.os.raw'; const prefix = 'he'; const suffix = 'llo'; - const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions'); - await getSuggestions({ fieldName, prefix, suffix }); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toBeCalledWith( - expect.any(String), - expect.any(Object), - prefix + suffix, - undefined, - undefined + + await getSuggestions({ fieldName: 'machine.os.raw', prefix, suffix }); + + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toHaveBeenCalledTimes(1); + expect(npStart.plugins.data.autocomplete.getValueSuggestions).toBeCalledWith( + expect.objectContaining({ + field: expect.any(Object), + query: prefix + suffix, + }) ); }); test('should escape quotes in suggestions', async () => { - const fieldName = 'machine.os'; // Has results with quotes in mock - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ fieldName: 'machine.os', prefix: '', suffix: '' }); + expect(suggestions[0].text).toEqual('"Windo\\"ws" '); expect(suggestions[1].text).toEqual('"Mac\'" '); expect(suggestions[2].text).toEqual('"Linux" '); }); test('should filter out string suggestions', async () => { - const fieldName = 'machine.os'; // Has results with quotes in mock - const prefix = 'banana'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ + fieldName: 'machine.os', + prefix: 'banana', + suffix: '', + }); + expect(suggestions.length).toEqual(0); }); test('should partially filter out string suggestions - case insensitive', async () => { - const fieldName = 'machine.os'; // Has results with quotes in mock - const prefix = 'ma'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + const suggestions = await getSuggestions({ + fieldName: 'machine.os', + prefix: 'ma', + suffix: '', + }); + expect(suggestions.length).toEqual(1); }); }); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts index ded66a7c6e8f0b..216e0f49ccd343 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts @@ -8,7 +8,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core import { Plugin as DataPublicPlugin } from '../../../../../src/plugins/data/public'; // @ts-ignore -import { kueryProvider } from './autocomplete_providers'; +import { setupKqlQuerySuggestionProvider } from './kql_query_suggestion'; /** @internal */ export interface KueryAutocompletePluginSetupDependencies { @@ -25,8 +25,10 @@ export class KueryAutocompletePlugin implements Plugin, void> { this.initializerContext = initializerContext; } - public async setup(core: CoreSetup, { data }: KueryAutocompletePluginSetupDependencies) { - data.autocomplete.addProvider(KUERY_LANGUAGE_NAME, kueryProvider); + public async setup(core: CoreSetup, plugins: KueryAutocompletePluginSetupDependencies) { + const kueryProvider = setupKqlQuerySuggestionProvider(core, plugins); + + plugins.data.autocomplete.addQuerySuggestionProvider(KUERY_LANGUAGE_NAME, kueryProvider); } public start(core: CoreStart) { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d38a23560fa9f7..4f679905fc352a 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; -import mappings from './mappings.json'; import { migrations } from './migrations'; import { initTelemetryCollection } from './server/maps_telemetry'; import { getAppTitle } from './common/i18n_getters'; -import _ from 'lodash'; import { MapPlugin } from './server/plugin'; import { APP_ID, APP_ICON, createMapPath, MAP_SAVED_OBJECT_TYPE } from './common/constants'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export function maps(kibana) { return new kibana.Plugin({ @@ -29,6 +30,7 @@ export function maps(kibana) { main: 'plugins/maps/legacy', icon: 'plugins/maps/icon.svg', euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.analyze, }, injectDefaultVars(server) { const serverConfig = server.config(); diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index f34e17f21ecd8b..ece775f5a7e25a 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -371,7 +371,9 @@ app.controller( if (prevIndexPatternIds !== nextIndexPatternIds) { return; } - $scope.indexPatterns = indexPatterns; + $scope.$evalAsync(() => { + $scope.indexPatterns = indexPatterns; + }); } $scope.isFullScreen = false; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js index df212f23cd8942..b04f8ff56e5ee4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js @@ -113,7 +113,11 @@ export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map(palette => { height: '100%', display: 'inline-block', }; - return
 
; + return ( +
+   +
+ ); }); return { value: palette.id, diff --git a/x-pack/legacy/plugins/ml/common/constants/field_types.ts b/x-pack/legacy/plugins/ml/common/constants/field_types.ts index 6c9af703c148a9..9402e4c20e46ff 100644 --- a/x-pack/legacy/plugins/ml/common/constants/field_types.ts +++ b/x-pack/legacy/plugins/ml/common/constants/field_types.ts @@ -16,3 +16,4 @@ export enum ML_JOB_FIELD_TYPES { } export const MLCATEGORY = 'mlcategory'; +export const DOC_COUNT = 'doc_count'; diff --git a/x-pack/legacy/plugins/ml/common/constants/new_job.ts b/x-pack/legacy/plugins/ml/common/constants/new_job.ts index 3c98b372afdf73..862fa72d11fdb7 100644 --- a/x-pack/legacy/plugins/ml/common/constants/new_job.ts +++ b/x-pack/legacy/plugins/ml/common/constants/new_job.ts @@ -26,7 +26,14 @@ export const DEFAULT_QUERY_DELAY = '60s'; export const SHARED_RESULTS_INDEX_NAME = 'shared'; +// Categorization export const NUMBER_OF_CATEGORY_EXAMPLES = 5; export const CATEGORY_EXAMPLES_SAMPLE_SIZE = 1000; export const CATEGORY_EXAMPLES_WARNING_LIMIT = 0.75; export const CATEGORY_EXAMPLES_ERROR_LIMIT = 0.02; + +export enum CATEGORY_EXAMPLES_VALIDATION_STATUS { + VALID = 'valid', + PARTIALLY_VALID = 'partially_valid', + INVALID = 'invalid', +} diff --git a/x-pack/legacy/plugins/ml/common/types/categories.ts b/x-pack/legacy/plugins/ml/common/types/categories.ts index 6ccd13ed9a39ec..765053ced52012 100644 --- a/x-pack/legacy/plugins/ml/common/types/categories.ts +++ b/x-pack/legacy/plugins/ml/common/types/categories.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../constants/new_job'; + export type CategoryId = number; export interface Category { @@ -23,3 +25,30 @@ export interface Token { type: string; position: number; } + +export interface CategorizationAnalyzer { + char_filter?: any[]; + tokenizer?: string; + filter?: any[]; + analyzer?: string; +} + +export interface CategoryFieldExample { + text: string; + tokens: Token[]; +} + +export enum VALIDATION_RESULT { + TOKEN_COUNT, + MEDIAN_LINE_LENGTH, + NULL_VALUES, + TOO_MANY_TOKENS, + FAILED_TO_TOKENIZE, + INSUFFICIENT_PRIVILEGES, +} + +export interface FieldExampleCheck { + id: VALIDATION_RESULT; + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS; + message: string; +} diff --git a/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts b/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts index aba2dbd230ada3..026c8e6110c993 100644 --- a/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/legacy/plugins/ml/common/util/string_utils.test.ts @@ -4,7 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderTemplate } from './string_utils'; +import { renderTemplate, getMedianStringLength } from './string_utils'; + +const strings: string[] = [ + 'foo', + 'foofoofoofoofoo', + 'foofoofoo', + 'f', + 'f', + 'foofoofoofoofoofoofoo', +]; +const noStrings: string[] = []; describe('ML - string utils', () => { describe('renderTemplate', () => { @@ -24,4 +34,16 @@ describe('ML - string utils', () => { expect(result).toBe('string with 1 replacement, and a 2nd one.'); }); }); + + describe('getMedianStringLength', () => { + test('test median for string array', () => { + const result = getMedianStringLength(strings); + expect(result).toBe(9); + }); + + test('test median for no strings', () => { + const result = getMedianStringLength(noStrings); + expect(result).toBe(0); + }); + }); }); diff --git a/x-pack/legacy/plugins/ml/common/util/string_utils.ts b/x-pack/legacy/plugins/ml/common/util/string_utils.ts index 432baabe773cc1..9dd2ce3d74cd5d 100644 --- a/x-pack/legacy/plugins/ml/common/util/string_utils.ts +++ b/x-pack/legacy/plugins/ml/common/util/string_utils.ts @@ -17,3 +17,8 @@ export function renderTemplate(str: string, data?: Record): stri return str; } + +export function getMedianStringLength(strings: string[]) { + const sortedStringLengths = strings.map(s => s.length).sort((a, b) => a - b); + return sortedStringLengths[Math.floor(sortedStringLengths.length / 2)] || 0; +} diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index c4289389b0d56d..fc1cec7c16208a 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -10,7 +10,7 @@ import KbnServer, { Server } from 'src/legacy/server/kbn_server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { plugin } from './server/new_platform'; import { CloudSetup } from '../../../plugins/cloud/server'; - +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; import { MlInitializerContext, MlCoreSetup, @@ -42,6 +42,7 @@ export const ml = (kibana: any) => { icon: 'plugins/ml/application/ml.svg', euiIconType: 'machineLearningApp', main: 'plugins/ml/legacy', + category: DEFAULT_APP_CATEGORIES.analyze, }, styleSheetPaths: resolve(__dirname, 'public/application/index.scss'), hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 48cf53cf1ac016..3b93213da40335 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -9,6 +9,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` Object { "field": "annotation", "name": "Annotation", + "scope": "row", "sortable": true, "width": "50%", }, diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 6c4e8925f369f9..3329bf1aab64a7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -323,6 +323,7 @@ const AnnotationsTable = injectI18n( }), sortable: true, width: '50%', + scope: 'row', }, { field: 'timestamp', diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 58f1214c11e103..23a40d9ecf295a 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -101,6 +101,7 @@ export function getColumns( defaultMessage: 'time', }), dataType: 'date', + scope: 'row', render: date => renderTime(date, interval), textOnly: true, sortable: true, diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js index 3591b0907ebb1f..e604c101a99940 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.js @@ -46,12 +46,8 @@ export class KqlFilterBar extends Component { const boolFilter = []; try { - const suggestions = await getSuggestions( - inputValue, - selectionStart, - indexPattern, - boolFilter - ); + const suggestions = + (await getSuggestions(inputValue, selectionStart, indexPattern, boolFilter)) || []; if (currentRequest !== this.currentRequest) { return; diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js index b1e79cbf55925b..4e74a4bd545a34 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/kql_filter_bar.test.js @@ -8,6 +8,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KqlFilterBar } from './kql_filter_bar'; +jest.mock('ui/new_platform'); + const defaultProps = { indexPattern: { title: '.ml-anomalies-*', diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js index c007e8bd05c5e5..bb7b143c948d83 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -6,23 +6,11 @@ import { npStart } from 'ui/new_platform'; import { esKuery } from '../../../../../../../../src/plugins/data/public'; -const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); - -export async function getSuggestions(query, selectionStart, indexPattern, boolFilter) { - const autocompleteProvider = getAutocompleteProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true, - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, +export function getSuggestions(query, selectionStart, indexPattern, boolFilter) { + return npStart.plugins.data.autocomplete.getQuerySuggestions({ + language: 'kuery', indexPatterns: [indexPattern], boolFilter, - }); - return getAutocompleteSuggestions({ query, selectionStart, selectionEnd: selectionStart, diff --git a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts index ee0540f6d58256..b85fb634891e56 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/ml_in_memory_table/types.ts @@ -28,6 +28,7 @@ export interface FieldDataColumnType { render?: RenderFunc; footer?: string | ReactElement | FooterFunc; textOnly?: boolean; + scope?: 'col' | 'row' | 'colgroup' | 'rowgroup'; 'data-test-subj'?: string; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx index ca6146f3e23b53..eb068f40716bc7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/top_nav/top_nav.tsx @@ -23,12 +23,14 @@ interface Duration { function getRecentlyUsedRangesFactory(timeHistory: TimeHistory) { return function(): Duration[] { - return timeHistory.get().map(({ from, to }: TimeRange) => { - return { - start: from, - end: to, - }; - }); + return ( + timeHistory.get()?.map(({ from, to }: TimeRange) => { + return { + start: from, + end: to, + }; + }) ?? [] + ); }; } @@ -54,9 +56,18 @@ export const TopNav: FC = () => { useEffect(() => { const subscriptions = new Subscription(); - subscriptions.add(timefilter.getRefreshIntervalUpdate$().subscribe(timefilterUpdateListener)); - subscriptions.add(timefilter.getTimeUpdate$().subscribe(timefilterUpdateListener)); - subscriptions.add(timefilter.getEnabledUpdated$().subscribe(timefilterUpdateListener)); + const refreshIntervalUpdate$ = timefilter.getRefreshIntervalUpdate$(); + if (refreshIntervalUpdate$ !== undefined) { + subscriptions.add(refreshIntervalUpdate$.subscribe(timefilterUpdateListener)); + } + const timeUpdate$ = timefilter.getTimeUpdate$(); + if (timeUpdate$ !== undefined) { + subscriptions.add(timeUpdate$.subscribe(timefilterUpdateListener)); + } + const enabledUpdated$ = timefilter.getEnabledUpdated$(); + if (enabledUpdated$ !== undefined) { + subscriptions.add(enabledUpdated$.subscribe(timefilterUpdateListener)); + } return function cleanup() { subscriptions.unsubscribe(); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 86f1324cc03772..34f281cec57d37 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -182,6 +182,7 @@ export const getColumns = ( sortable: true, truncateText: true, 'data-test-subj': 'mlAnalyticsTableColumnId', + scope: 'row', }, { field: DataFrameAnalyticsListColumn.description, diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts index b8df021990f584..a85674986c7f7f 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.d.ts @@ -15,6 +15,7 @@ import { AppStateSelectedCells } from '../explorer/explorer_utils'; declare interface ExplorerProps { explorerState: ExplorerState; + severity: number; showCharts: boolean; setSelectedCells: (swimlaneSelectedCells: AppStateSelectedCells) => void; } diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js index 79071319965781..6a1c5339de1f56 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js @@ -96,6 +96,7 @@ export class Explorer extends React.Component { static propTypes = { explorerState: PropTypes.object.isRequired, setSelectedCells: PropTypes.func.isRequired, + severity: PropTypes.number.isRequired, showCharts: PropTypes.bool.isRequired, }; @@ -260,7 +261,7 @@ export class Explorer extends React.Component { }; render() { - const { showCharts } = this.props; + const { showCharts, severity } = this.props; const { annotationsData, @@ -276,7 +277,6 @@ export class Explorer extends React.Component { queryString, selectedCells, selectedJobs, - severity, swimlaneContainerWidth, tableData, tableQueryString, diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 583375c87007e4..a255b6b0434e4e 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -53,7 +53,7 @@ export const ExplorerChartSingleMetric = injectI18n( static propTypes = { tooManyBuckets: PropTypes.bool, seriesConfig: PropTypes.object, - severity: PropTypes.number, + severity: PropTypes.number.isRequired, }; componentDidMount() { @@ -312,13 +312,16 @@ export const ExplorerChartSingleMetric = injectI18n( }) .on('mouseout', () => mlChartTooltipService.hide()); + const isAnomalyVisible = d => + _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; + // Update all dots to new positions. dots .attr('cx', d => lineChartXScale(d.date)) .attr('cy', d => lineChartYScale(d.value)) .attr('class', d => { let markerClass = 'metric-value'; - if (_.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity) { + if (isAnomalyVisible(d)) { markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore).id}`; } return markerClass; @@ -328,9 +331,7 @@ export const ExplorerChartSingleMetric = injectI18n( const multiBucketMarkers = lineChartGroup .select('.chart-markers') .selectAll('.multi-bucket') - .data( - data.filter(d => d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true) - ); + .data(data.filter(d => isAnomalyVisible(d) && showMultiBucketAnomalyMarker(d) === true)); // Remove multi-bucket markers that are no longer needed multiBucketMarkers.exit().remove(); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js index 4fb4e7d4df94f2..14d356c0d1c81c 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.js @@ -280,11 +280,13 @@ export function loadViewByTopFieldValuesForSelectedTime( const topFieldValues = []; const topInfluencers = resp.influencers[viewBySwimlaneFieldName]; - topInfluencers.forEach(influencerData => { - if (influencerData.maxAnomalyScore > 0) { - topFieldValues.push(influencerData.influencerFieldValue); - } - }); + if (Array.isArray(topInfluencers)) { + topInfluencers.forEach(influencerData => { + if (influencerData.maxAnomalyScore > 0) { + topFieldValues.push(influencerData.influencerFieldValue); + } + }); + } resolve(topFieldValues); }); } else { diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts index 99b9aceab3696f..663a3f3d8f9556 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts @@ -20,6 +20,7 @@ describe('ML - roundToDecimalPlace formatter', () => { expect(roundToDecimalPlace(0.0005)).toBe('5.00e-4'); expect(roundToDecimalPlace(-0.0005)).toBe('-5.00e-4'); expect(roundToDecimalPlace(-12.045)).toBe(-12.04); + expect(roundToDecimalPlace(0)).toBe(0); }); it('returns the correct format using specified decimal place', () => { @@ -31,5 +32,6 @@ describe('ML - roundToDecimalPlace formatter', () => { expect(roundToDecimalPlace(0.0005, 4)).toBe(0.0005); expect(roundToDecimalPlace(0.00005, 4)).toBe('5.00e-5'); expect(roundToDecimalPlace(-0.00005, 4)).toBe('-5.00e-5'); + expect(roundToDecimalPlace(0, 4)).toBe(0); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts index f863fe6d76e57d..5a030d7619e986 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export function roundToDecimalPlace(num: number, dp: number = 2) { +export function roundToDecimalPlace(num: number, dp: number = 2): number | string { + if (num % 1 === 0) { + // no decimal place + return num; + } + if (Math.abs(num) < Math.pow(10, -dp)) { return Number.parseFloat(String(num)).toExponential(2); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 1adb1e311dc682..e70198b36e0df6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -204,6 +204,7 @@ class ForecastsTableUI extends Component { render: date => formatDate(date, TIME_FORMAT), textOnly: true, sortable: true, + scope: 'row', }, { field: 'forecast_start_timestamp', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index eb0a905725d750..9984f3be299ae8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -6,6 +6,7 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; import { toLocaleString } from '../../../../util/string_utils'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -35,6 +36,8 @@ export function formatValues([key, value]) { case 'established_model_memory': case 'input_bytes': case 'model_bytes': + case 'model_bytes_exceeded': + case 'model_bytes_memory_limit': value = formatData(value); break; @@ -53,9 +56,16 @@ export function formatValues([key, value]) { case 'total_over_field_count': case 'total_partition_field_count': case 'bucket_allocation_failures_count': + case 'search_count': value = toLocaleString(value); break; + // numbers rounded to 3 decimal places + case 'average_search_time_per_bucket_ms': + case 'exponential_average_search_time_per_hour_ms': + value = typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value; + break; + default: break; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 7a6a1c22e39c5a..b691bc34295c58 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -172,6 +172,7 @@ class JobsListUI extends Component { sortable: true, truncateText: false, width: '20%', + scope: 'row', render: isManagementTable ? id => this.getJobIdLink(id) : undefined, }, { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index b0eb1b98cd02b8..4530c00c105355 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -183,7 +183,7 @@ export class AdvancedJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); - const detectors = getRichDetectors(job, datafeed, this.scriptFields, true); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, true); // keep track of the custom rules for each detector const customRules = this._detectors.map(d => d.custom_rules); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts index 7c070ccc6bc534..0ff0ffb6f3bb39 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts @@ -16,25 +16,31 @@ import { CREATED_BY_LABEL, DEFAULT_BUCKET_SPAN, DEFAULT_RARE_BUCKET_SPAN, + CATEGORY_EXAMPLES_VALIDATION_STATUS, } from '../../../../../../common/constants/new_job'; import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; +import { + CategorizationAnalyzer, + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../../../common/types/categories'; import { getRichDetectors } from './util/general'; -import { CategorizationExamplesLoader, CategoryExample } from '../results_loader'; -import { CategorizationAnalyzer, getNewJobDefaults } from '../../../../services/ml_server_info'; - -type CategorizationAnalyzerType = CategorizationAnalyzer | null; +import { CategorizationExamplesLoader } from '../results_loader'; +import { getNewJobDefaults } from '../../../../services/ml_server_info'; export class CategorizationJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.CATEGORIZATION; private _createCountDetector: () => void = () => {}; private _createRareDetector: () => void = () => {}; private _examplesLoader: CategorizationExamplesLoader; - private _categoryFieldExamples: CategoryExample[] = []; - private _categoryFieldValid: number = 0; + private _categoryFieldExamples: CategoryFieldExample[] = []; + private _validationChecks: FieldExampleCheck[] = []; + private _overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS = + CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; private _detectorType: ML_JOB_AGGREGATION.COUNT | ML_JOB_AGGREGATION.RARE = ML_JOB_AGGREGATION.COUNT; - private _categorizationAnalyzer: CategorizationAnalyzerType = null; - private _defaultCategorizationAnalyzer: CategorizationAnalyzerType; + private _categorizationAnalyzer: CategorizationAnalyzer = {}; + private _defaultCategorizationAnalyzer: CategorizationAnalyzer; constructor( indexPattern: IndexPattern, @@ -46,7 +52,7 @@ export class CategorizationJobCreator extends JobCreator { this._examplesLoader = new CategorizationExamplesLoader(this, indexPattern, query); const { anomaly_detectors: anomalyDetectors } = getNewJobDefaults(); - this._defaultCategorizationAnalyzer = anomalyDetectors.categorization_analyzer || null; + this._defaultCategorizationAnalyzer = anomalyDetectors.categorization_analyzer || {}; } public setDefaultDetectorProperties( @@ -93,7 +99,7 @@ export class CategorizationJobCreator extends JobCreator { } else { delete this._job_config.analysis_config.categorization_field_name; this._categoryFieldExamples = []; - this._categoryFieldValid = 0; + this._validationChecks = []; } } @@ -102,31 +108,38 @@ export class CategorizationJobCreator extends JobCreator { } public async loadCategorizationFieldExamples() { - const { valid, examples, sampleSize } = await this._examplesLoader.loadExamples(); + const { + examples, + sampleSize, + overallValidStatus, + validationChecks, + } = await this._examplesLoader.loadExamples(); this._categoryFieldExamples = examples; - this._categoryFieldValid = valid; - return { valid, examples, sampleSize }; + this._validationChecks = validationChecks; + this._overallValidStatus = overallValidStatus; + return { examples, sampleSize, overallValidStatus, validationChecks }; } public get categoryFieldExamples() { return this._categoryFieldExamples; } - public get categoryFieldValid() { - return this._categoryFieldValid; + public get validationChecks() { + return this._validationChecks; + } + + public get overallValidStatus() { + return this._overallValidStatus; } public get selectedDetectorType() { return this._detectorType; } - public set categorizationAnalyzer(analyzer: CategorizationAnalyzerType) { + public set categorizationAnalyzer(analyzer: CategorizationAnalyzer) { this._categorizationAnalyzer = analyzer; - if ( - analyzer === null || - isEqual(this._categorizationAnalyzer, this._defaultCategorizationAnalyzer) - ) { + if (isEqual(this._categorizationAnalyzer, this._defaultCategorizationAnalyzer)) { delete this._job_config.analysis_config.categorization_analyzer; } else { this._job_config.analysis_config.categorization_analyzer = analyzer; @@ -140,7 +153,7 @@ export class CategorizationJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.CATEGORIZATION; - const detectors = getRichDetectors(job, datafeed, this.scriptFields, false); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); const dtr = detectors[0]; if (detectors.length && dtr.agg !== null && dtr.field !== null) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 513c8239db01e0..90c189c9d61971 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -19,7 +19,7 @@ import { CREATED_BY_LABEL, SHARED_RESULTS_INDEX_NAME, } from '../../../../../../common/constants/new_job'; -import { isSparseDataJob } from './util/general'; +import { isSparseDataJob, collectAggs } from './util/general'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { Calendar } from '../../../../../../common/types/calendars'; import { mlCalendarService } from '../../../../services/calendar_service'; @@ -43,6 +43,7 @@ export class JobCreator { protected _aggs: Aggregation[] = []; protected _fields: Field[] = []; protected _scriptFields: Field[] = []; + protected _aggregationFields: Field[] = []; protected _sparseData: boolean = false; private _stopAllRefreshPolls: { stop: boolean; @@ -450,6 +451,14 @@ export class JobCreator { return this._scriptFields; } + public get aggregationFields(): Field[] { + return this._aggregationFields; + } + + public get additionalFields(): Field[] { + return [...this._scriptFields, ...this._aggregationFields]; + } + public get subscribers(): ProgressSubscriber[] { return this._subscribers; } @@ -603,6 +612,7 @@ export class JobCreator { } this._sparseData = isSparseDataJob(job, datafeed); + this._scriptFields = []; if (this._datafeed_config.script_fields !== undefined) { this._scriptFields = Object.keys(this._datafeed_config.script_fields).map(f => ({ id: f, @@ -610,8 +620,11 @@ export class JobCreator { type: ES_FIELD_TYPES.KEYWORD, aggregatable: true, })); - } else { - this._scriptFields = []; + } + + this._aggregationFields = []; + if (this._datafeed_config.aggregations?.buckets !== undefined) { + collectAggs(this._datafeed_config.aggregations.buckets, this._aggregationFields); } } } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 8a4411bf9025f7..7c5fba028d9e8f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -153,7 +153,7 @@ export class MultiMetricJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; - const detectors = getRichDetectors(job, datafeed, this.scriptFields, false); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); if (datafeed.aggregations !== undefined) { // if we've converting from a single metric job, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 9300e53c578e1b..3009d68ca67caa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -135,7 +135,7 @@ export class PopulationJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.POPULATION; - const detectors = getRichDetectors(job, datafeed, this.scriptFields, false); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); this.removeAllDetectors(); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index f98fd4dbe970a5..9f3500185c2bfa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -190,7 +190,7 @@ export class SingleMetricJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; - const detectors = getRichDetectors(job, datafeed, this.scriptFields, false); + const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); this.removeAllDetectors(); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 6443539a9877da..e5b6212a4326e5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -11,7 +11,8 @@ import { ML_JOB_AGGREGATION, SPARSE_DATA_AGGREGATIONS, } from '../../../../../../../common/constants/aggregation_types'; -import { MLCATEGORY } from '../../../../../../../common/constants/field_types'; +import { MLCATEGORY, DOC_COUNT } from '../../../../../../../common/constants/field_types'; +import { ES_FIELD_TYPES } from '../../../../../../../../../../../src/plugins/data/public'; import { EVENT_RATE_FIELD_ID, Field, @@ -27,14 +28,14 @@ import { } from '../index'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; -const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => { +const getFieldByIdFactory = (additionalFields: Field[]) => (id: string) => { let field = newJobCapsService.getFieldById(id); // if no field could be found it may be a pretend field, like mlcategory or a script field if (field === null) { if (id === MLCATEGORY) { field = mlCategory; - } else if (scriptFields.length) { - field = scriptFields.find(f => f.id === id) || null; + } else if (additionalFields.length) { + field = additionalFields.find(f => f.id === id) || null; } } return field; @@ -44,12 +45,12 @@ const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => { export function getRichDetectors( job: Job, datafeed: Datafeed, - scriptFields: Field[], + additionalFields: Field[], advanced: boolean = false ) { const detectors = advanced ? getDetectorsAdvanced(job, datafeed) : getDetectors(job, datafeed); - const getFieldById = getFieldByIdFactory(scriptFields); + const getFieldById = getFieldByIdFactory(additionalFields); return detectors.map(d => { let field = null; @@ -82,19 +83,19 @@ export function getRichDetectors( }); } -export function createFieldOptions(fields: Field[]) { - return fields - .filter(f => f.id !== EVENT_RATE_FIELD_ID) - .map(f => ({ - label: f.name, - })) - .sort((a, b) => a.label.localeCompare(b.label)); -} - -export function createScriptFieldOptions(scriptFields: Field[]) { - return scriptFields.map(f => ({ - label: f.id, - })); +export function createFieldOptions(fields: Field[], additionalFields: Field[]) { + return [ + ...fields + .filter(f => f.id !== EVENT_RATE_FIELD_ID) + .map(f => ({ + label: f.name, + })), + ...additionalFields + .filter(f => fields.some(f2 => f2.id === f.id) === false) + .map(f => ({ + label: f.id, + })), + ].sort((a, b) => a.label.localeCompare(b.label)); } export function createMlcategoryFieldOption(categorizationFieldName: string | null) { @@ -108,6 +109,16 @@ export function createMlcategoryFieldOption(categorizationFieldName: string | nu ]; } +export function createDocCountFieldOption(usingAggregations: boolean) { + return usingAggregations + ? [ + { + label: DOC_COUNT, + }, + ] + : []; +} + function getDetectorsAdvanced(job: Job, datafeed: Datafeed) { return processFieldlessAggs(job.analysis_config.detectors); } @@ -305,3 +316,26 @@ export function getJobCreatorTitle(jobCreator: JobCreatorType) { return ''; } } + +// recurse through a datafeed aggregation object, +// adding top level keys from each nested agg to an array +// of fields +export function collectAggs(o: any, aggFields: Field[]) { + for (const i in o) { + if (o[i] !== null && typeof o[i] === 'object') { + if (i === 'aggregations' || i === 'aggs') { + Object.keys(o[i]).forEach(k => { + if (k !== 'aggregations' && i !== 'aggs') { + aggFields.push({ + id: k, + name: k, + type: ES_FIELD_TYPES.KEYWORD, + aggregatable: true, + }); + } + }); + } + collectAggs(o[i], aggFields); + } + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 976e94b377ae8b..8f6b16c407fb66 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -16,7 +16,7 @@ import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_c import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; import { cardinalityValidator, CardinalityValidatorResult } from './validators'; -import { CATEGORY_EXAMPLES_ERROR_LIMIT } from '../../../../../../common/constants/new_job'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../common/constants/new_job'; // delay start of validation to allow the user to make changes // e.g. if they are typing in a new value, try not to validate @@ -207,7 +207,7 @@ export class JobValidator { private _runAdvancedValidation() { if (isCategorizationJobCreator(this._jobCreator)) { this._advancedValidations.categorizationFieldValid.valid = - this._jobCreator.categoryFieldValid > CATEGORY_EXAMPLES_ERROR_LIMIT; + this._jobCreator.overallValidStatus !== CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; } } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts index ce1ea0bdaf181c..62a4d070fec328 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts @@ -6,15 +6,12 @@ import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; -import { Token } from '../../../../../../common/types/categories'; import { CategorizationJobCreator } from '../job_creator'; import { ml } from '../../../../services/ml_api_service'; -import { NUMBER_OF_CATEGORY_EXAMPLES } from '../../../../../../common/constants/new_job'; - -export interface CategoryExample { - text: string; - tokens: Token[]; -} +import { + NUMBER_OF_CATEGORY_EXAMPLES, + CATEGORY_EXAMPLES_VALIDATION_STATUS, +} from '../../../../../../common/constants/new_job'; export class CategorizationExamplesLoader { private _jobCreator: CategorizationJobCreator; @@ -36,20 +33,22 @@ export class CategorizationExamplesLoader { const analyzer = this._jobCreator.categorizationAnalyzer; const categorizationFieldName = this._jobCreator.categorizationFieldName; if (categorizationFieldName === null) { - return { valid: 0, examples: [], sampleSize: 0 }; + return { + examples: [], + sampleSize: 0, + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID, + validationChecks: [], + }; } - const start = Math.floor( - this._jobCreator.start + (this._jobCreator.end - this._jobCreator.start) / 2 - ); const resp = await ml.jobs.categorizationFieldExamples( this._indexPatternTitle, this._query, NUMBER_OF_CATEGORY_EXAMPLES, categorizationFieldName, this._timeFieldName, - start, - 0, + this._jobCreator.start, + this._jobCreator.end, analyzer ); return resp; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts index 724c62f22e469f..e15d859f8e6c31 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/index.ts @@ -5,4 +5,4 @@ */ export { ResultsLoader, Results, ModelItem, Anomaly } from './results_loader'; -export { CategorizationExamplesLoader, CategoryExample } from './categorization_examples_loader'; +export { CategorizationExamplesLoader } from './categorization_examples_loader'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx index f2e25168668352..9af1226d1fe6c7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, useContext } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions } from '../../../../../common/job_creator/util/general'; @@ -17,7 +18,8 @@ interface Props { } export const TimeFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { - const options: EuiComboBoxOptionProps[] = createFieldOptions(fields); + const { jobCreator } = useContext(JobCreatorContext); + const options: EuiComboBoxOptionProps[] = createFieldOptions(fields, jobCreator.additionalFields); const selection: EuiComboBoxOptionProps[] = []; if (selectedField !== null) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 06c8068a9c0052..753cea7adcb353 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -18,7 +18,6 @@ import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; import { createFieldOptions, - createScriptFieldOptions, createMlcategoryFieldOption, } from '../../../../../common/job_creator/util/general'; import { @@ -88,7 +87,7 @@ export const AdvancedDetectorModal: FC = ({ const [fieldOptionEnabled, setFieldOptionEnabled] = useState(true); const { descriptionPlaceholder, setDescriptionPlaceholder } = useDetectorPlaceholder(detector); - const usingScriptFields = jobCreator.scriptFields.length > 0; + const usingScriptFields = jobCreator.additionalFields.length > 0; // list of aggregation combobox options. const aggOptions: EuiComboBoxOptionProps[] = aggs @@ -98,12 +97,12 @@ export const AdvancedDetectorModal: FC = ({ // fields available for the selected agg const { currentFieldOptions, setCurrentFieldOptions } = useCurrentFieldOptions( detector.agg, - jobCreator.scriptFields + jobCreator.additionalFields, + fields ); const allFieldOptions: EuiComboBoxOptionProps[] = [ - ...createFieldOptions(fields), - ...createScriptFieldOptions(jobCreator.scriptFields), + ...createFieldOptions(fields, jobCreator.additionalFields), ].sort(comboBoxOptionsSort); const splitFieldOptions: EuiComboBoxOptionProps[] = [ @@ -127,7 +126,9 @@ export const AdvancedDetectorModal: FC = ({ return mlCategory; } return ( - fields.find(f => f.id === title) || jobCreator.scriptFields.find(f => f.id === title) || null + fields.find(f => f.id === title) || + jobCreator.additionalFields.find(f => f.id === title) || + null ); } @@ -365,21 +366,27 @@ function useDetectorPlaceholder(detector: RichDetector) { } // creates list of combobox options based on an aggregation's field list -function createFieldOptionsFromAgg(agg: Aggregation | null) { - return createFieldOptions(agg !== null && agg.fields !== undefined ? agg.fields : []); +function createFieldOptionsFromAgg(agg: Aggregation | null, additionalFields: Field[]) { + return createFieldOptions( + agg !== null && agg.fields !== undefined ? agg.fields : [], + additionalFields + ); } // custom hook for storing combobox options based on an aggregation field list -function useCurrentFieldOptions(aggregation: Aggregation | null, scriptFields: Field[]) { +function useCurrentFieldOptions( + aggregation: Aggregation | null, + additionalFields: Field[], + fields: Field[] +) { const [currentFieldOptions, setCurrentFieldOptions] = useState( - createFieldOptionsFromAgg(aggregation) + createFieldOptionsFromAgg(aggregation, additionalFields) ); - const scriptFieldOptions = createScriptFieldOptions(scriptFields); return { currentFieldOptions, setCurrentFieldOptions: (agg: Aggregation | null) => - setCurrentFieldOptions([...createFieldOptionsFromAgg(agg), ...scriptFieldOptions]), + setCurrentFieldOptions(createFieldOptionsFromAgg(agg, additionalFields)), }; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx index d995d40284abae..6451c2785eae0b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx @@ -9,10 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; -import { - createFieldOptions, - createScriptFieldOptions, -} from '../../../../../common/job_creator/util/general'; +import { createFieldOptions } from '../../../../../common/job_creator/util/general'; interface Props { fields: Field[]; @@ -23,8 +20,7 @@ interface Props { export const CategorizationFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { const { jobCreator } = useContext(JobCreatorContext); const options: EuiComboBoxOptionProps[] = [ - ...createFieldOptions(fields), - ...createScriptFieldOptions(jobCreator.scriptFields), + ...createFieldOptions(fields, jobCreator.additionalFields), ]; const selection: EuiComboBoxOptionProps[] = []; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx index 270ba99d938cdc..ac886a3aea61a7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/examples_valid_callout.tsx @@ -9,27 +9,24 @@ import { EuiCallOut, EuiSpacer, EuiCallOutProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CategorizationAnalyzer } from '../../../../../../../services/ml_server_info'; -import { EditCategorizationAnalyzerFlyout } from '../../../common/edit_categorization_analyzer_flyout'; import { - CATEGORY_EXAMPLES_ERROR_LIMIT, - CATEGORY_EXAMPLES_WARNING_LIMIT, -} from '../../../../../../../../../common/constants/new_job'; - -type CategorizationAnalyzerType = CategorizationAnalyzer | null; + CategorizationAnalyzer, + FieldExampleCheck, +} from '../../../../../../../../../common/types/categories'; +import { EditCategorizationAnalyzerFlyout } from '../../../common/edit_categorization_analyzer_flyout'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../../../../common/constants/new_job'; interface Props { - examplesValid: number; - sampleSize: number; - categorizationAnalyzer: CategorizationAnalyzerType; + validationChecks: FieldExampleCheck[]; + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; + categorizationAnalyzer: CategorizationAnalyzer; } export const ExamplesValidCallout: FC = ({ - examplesValid, + overallValidStatus, + validationChecks, categorizationAnalyzer, - sampleSize, }) => { - const percentageText = ; const analyzerUsed = ; let color: EuiCallOutProps['color'] = 'success'; @@ -40,7 +37,7 @@ export const ExamplesValidCallout: FC = ({ } ); - if (examplesValid < CATEGORY_EXAMPLES_ERROR_LIMIT) { + if (overallValidStatus === CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID) { color = 'danger'; title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldCalloutTitle.invalid', @@ -48,7 +45,7 @@ export const ExamplesValidCallout: FC = ({ defaultMessage: 'Selected category field is invalid', } ); - } else if (examplesValid < CATEGORY_EXAMPLES_WARNING_LIMIT) { + } else if (overallValidStatus === CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID) { color = 'warning'; title = i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.categorizationFieldCalloutTitle.possiblyInvalid', @@ -60,45 +57,24 @@ export const ExamplesValidCallout: FC = ({ return ( - {percentageText} + {validationChecks.map((v, i) => ( +
{v.message}
+ ))} {analyzerUsed}
); }; -const PercentageText: FC<{ examplesValid: number; sampleSize: number }> = ({ - examplesValid, - sampleSize, -}) => ( -
- -
-); - -const AnalyzerUsed: FC<{ categorizationAnalyzer: CategorizationAnalyzerType }> = ({ +const AnalyzerUsed: FC<{ categorizationAnalyzer: CategorizationAnalyzer }> = ({ categorizationAnalyzer, }) => { let analyzer = ''; - if (typeof categorizationAnalyzer === null) { - return null; - } - if (typeof categorizationAnalyzer === 'string') { - analyzer = categorizationAnalyzer; - } else { - if (categorizationAnalyzer?.tokenizer !== undefined) { - analyzer = categorizationAnalyzer?.tokenizer!; - } else if (categorizationAnalyzer?.analyzer !== undefined) { - analyzer = categorizationAnalyzer?.analyzer!; - } + if (categorizationAnalyzer?.tokenizer !== undefined) { + analyzer = categorizationAnalyzer.tokenizer; + } else if (categorizationAnalyzer?.analyzer !== undefined) { + analyzer = categorizationAnalyzer.analyzer; } return ( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx index 7f9b2e43b90050..51cea179a6c0d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/field_examples.tsx @@ -7,10 +7,10 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiText } from '@elastic/eui'; -import { CategoryExample } from '../../../../../common/results_loader'; +import { CategoryFieldExample } from '../../../../../../../../../common/types/categories'; interface Props { - fieldExamples: CategoryExample[] | null; + fieldExamples: CategoryFieldExample[] | null; } const TOKEN_HIGHLIGHT_COLOR = '#b0ccf7'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx index 52b5c61e70fe5e..411f6e898bd486 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx @@ -14,7 +14,11 @@ import { CategorizationField } from '../categorization_field'; import { CategorizationDetector } from '../categorization_detector'; import { FieldExamples } from './field_examples'; import { ExamplesValidCallout } from './examples_valid_callout'; -import { CategoryExample } from '../../../../../common/results_loader'; +import { + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../../../../../../common/types/categories'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../../../../common/constants/new_job'; import { LoadingWrapper } from '../../../charts/loading_wrapper'; interface Props { @@ -31,9 +35,11 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { const [categorizationAnalyzerString, setCategorizationAnalyzerString] = useState( JSON.stringify(jobCreator.categorizationAnalyzer) ); - const [fieldExamples, setFieldExamples] = useState(null); - const [examplesValid, setExamplesValid] = useState(0); - const [sampleSize, setSampleSize] = useState(0); + const [fieldExamples, setFieldExamples] = useState(null); + const [overallValidStatus, setOverallValidStatus] = useState( + CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID + ); + const [validationChecks, setValidationChecks] = useState([]); const [categorizationFieldName, setCategorizationFieldName] = useState( jobCreator.categorizationFieldName @@ -73,28 +79,32 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { setLoadingData(true); try { const { - valid, examples, - sampleSize: tempSampleSize, + overallValidStatus: tempOverallValidStatus, + validationChecks: tempValidationChecks, } = await jobCreator.loadCategorizationFieldExamples(); setFieldExamples(examples); - setExamplesValid(valid); + setOverallValidStatus(tempOverallValidStatus); + setValidationChecks(tempValidationChecks); setLoadingData(false); - setSampleSize(tempSampleSize); } catch (error) { setLoadingData(false); + setFieldExamples(null); + setValidationChecks([]); + setOverallValidStatus(CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID); mlMessageBarService.notify.error(error); } } else { setFieldExamples(null); - setExamplesValid(0); + setValidationChecks([]); + setOverallValidStatus(CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID); } setIsValid(categorizationFieldName !== null); } useEffect(() => { jobCreatorUpdate(); - }, [examplesValid]); + }, [overallValidStatus]); return ( <> @@ -109,8 +119,8 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { {fieldExamples !== null && loadingData === false && ( <> diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 639bdb9ec76bfd..d4ac470f4ea4fa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -11,7 +11,6 @@ import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, - createScriptFieldOptions, createMlcategoryFieldOption, } from '../../../../../common/job_creator/util/general'; @@ -24,8 +23,7 @@ interface Props { export const InfluencersSelect: FC = ({ fields, changeHandler, selectedInfluencers }) => { const { jobCreator } = useContext(JobCreatorContext); const options: EuiComboBoxOptionProps[] = [ - ...createFieldOptions(fields), - ...createScriptFieldOptions(jobCreator.scriptFields), + ...createFieldOptions(fields, jobCreator.additionalFields), ...createMlcategoryFieldOption(jobCreator.categorizationFieldName), ]; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx index efe32e3173cade..6fe3aaf0a86526 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx @@ -11,7 +11,7 @@ import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; import { createFieldOptions, - createScriptFieldOptions, + createDocCountFieldOption, } from '../../../../../common/job_creator/util/general'; interface Props { @@ -23,8 +23,8 @@ interface Props { export const SummaryCountFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { const { jobCreator } = useContext(JobCreatorContext); const options: EuiComboBoxOptionProps[] = [ - ...createFieldOptions(fields), - ...createScriptFieldOptions(jobCreator.scriptFields), + ...createFieldOptions(fields, jobCreator.additionalFields), + ...createDocCountFieldOption(jobCreator.aggregationFields.length > 0), ]; const selection: EuiComboBoxOptionProps[] = []; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 141ed5d1bbb8ff..c4a96d9e373c8d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -77,6 +77,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const [kibanaObjects, setKibanaObjects] = useState({}); const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); const [resultsUrl, setResultsUrl] = useState(''); + const [existingGroups, setExistingGroups] = useState(existingGroupIds); // #endregion const { @@ -109,6 +110,10 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { setKibanaObjects(kibanaObjectsResult); setSaveState(SAVE_STATE.NOT_SAVED); + + // mix existing groups from the server with the groups used across all jobs in the module. + const moduleGroups = [...response.jobs.map(j => j.config.groups || [])].flat(); + setExistingGroups([...new Set([...existingGroups, ...moduleGroups])]); } catch (e) { // eslint-disable-next-line no-console console.error(e); @@ -222,6 +227,12 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { ...jobOverrides, [job.job_id as string]: job, }); + if (job.groups !== undefined) { + // add newly added jobs to the list of existing groups + // for use when editing other jobs in the module + const groups = [...new Set([...existingGroups, ...job.groups])]; + setExistingGroups(groups); + } }; const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); @@ -304,7 +315,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { jobs={jobs} jobPrefix={jobPrefix} saveState={saveState} - existingGroupIds={existingGroupIds} + existingGroupIds={existingGroups} jobOverrides={jobOverrides} onJobOverridesChange={onJobOverridesChange} /> diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx index 6aaad5294369b7..633efc2856dac1 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -184,6 +184,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim explorerState, setSelectedCells, showCharts, + severity: tableSeverity.val, }} />
diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts index 89ed35d5588f2d..44111ae32cd305 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts @@ -10,6 +10,6 @@ export * from './new_job'; export * from './datavisualizer'; export * from './settings'; export * from './data_frame_analytics'; -export * from './timeseriesexplorer'; +export { timeSeriesExplorerRoute } from './timeseriesexplorer'; export * from './explorer'; export * from './access_denied'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx new file mode 100644 index 00000000000000..6917ec718d3a8d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -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 React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { render } from '@testing-library/react'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer'; + +jest.mock('ui/new_platform'); + +describe('TimeSeriesExplorerUrlStateManager', () => { + test('Initial render shows "No single metric jobs found"', () => { + const props = { + config: { get: () => 'Browser' }, + jobsWithTimeRange: [], + }; + + const { container } = render( + + + + + + ); + + expect(container.textContent).toContain('No single metric jobs found'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index cbf54a70ea74f4..f824faf7845c64 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -26,7 +26,10 @@ import { APP_STATE_ACTION } from '../../timeseriesexplorer/timeseriesexplorer_co import { createTimeSeriesJobData, getAutoZoomDuration, + validateJobSelection, } from '../../timeseriesexplorer/timeseriesexplorer_utils'; +import { TimeSeriesExplorerPage } from '../../timeseriesexplorer/timeseriesexplorer_page'; +import { TimeseriesexplorerNoJobsFound } from '../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found'; import { useUrlState } from '../../util/url_state'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; @@ -74,13 +77,14 @@ interface TimeSeriesExplorerUrlStateManager { jobsWithTimeRange: MlJobWithTimeRange[]; } -const TimeSeriesExplorerUrlStateManager: FC = ({ +export const TimeSeriesExplorerUrlStateManager: FC = ({ config, jobsWithTimeRange, }) => { const [appState, setAppState] = useUrlState('_a'); const [globalState, setGlobalState] = useUrlState('_g'); const [lastRefresh, setLastRefresh] = useState(0); + const [selectedJobId, setSelectedJobId] = useState(); const refresh = useRefresh(); useEffect(() => { @@ -102,23 +106,27 @@ const TimeSeriesExplorerUrlStateManager: FC = timefilter.enableAutoRefreshSelector(); }, []); + // We cannot simply infer bounds from the globalState's `time` attribute + // with `moment` since it can contain custom strings such as `now-15m`. + // So when globalState's `time` changes, we update the timefilter and use + // `timefilter.getBounds()` to update `bounds` in this component's state. + const [bounds, setBounds] = useState(undefined); useEffect(() => { if (globalState?.time !== undefined) { timefilter.setTime({ from: globalState.time.from, to: globalState.time.to, }); + + const timefilterBounds = timefilter.getBounds(); + // Only if both min/max bounds are valid moment times set the bounds. + // An invalid string restored from globalState might return `undefined`. + if (timefilterBounds?.min !== undefined && timefilterBounds?.max !== undefined) { + setBounds(timefilter.getBounds()); + } } }, [globalState?.time?.from, globalState?.time?.to]); - let bounds: TimeRangeBounds | undefined; - if (globalState?.time !== undefined) { - bounds = { - min: moment(globalState.time.from), - max: moment(globalState.time.to), - }; - } - const selectedJobIds = globalState?.ml?.jobIds; // Sort selectedJobIds so we can be sure comparison works when stringifying. if (Array.isArray(selectedJobIds)) { @@ -137,26 +145,31 @@ const TimeSeriesExplorerUrlStateManager: FC = setLastRefresh(Date.now()); appStateHandler(APP_STATE_ACTION.CLEAR); } + const validatedJobId = validateJobSelection(jobsWithTimeRange, selectedJobIds, setGlobalState); + if (typeof validatedJobId === 'string') { + setSelectedJobId(validatedJobId); + } }, [JSON.stringify(selectedJobIds)]); // Next we get globalState and appState information to pass it on as props later. - // If a job change is going on, we fall back to defaults (as if appState was already cleard), + // If a job change is going on, we fall back to defaults (as if appState was already cleared), // otherwise the page could break. const selectedDetectorIndex = isJobChange ? 0 : +appState?.mlTimeSeriesExplorer?.detectorIndex || 0; const selectedEntities = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.entities; const selectedForecastId = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.forecastId; - const zoom = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.zoom; + const zoom: { + from: string; + to: string; + } = isJobChange ? undefined : appState?.mlTimeSeriesExplorer?.zoom; - const selectedJob = selectedJobIds && mlJobService.getJob(selectedJobIds[0]); + const selectedJob = selectedJobId !== undefined ? mlJobService.getJob(selectedJobId) : undefined; + const timeSeriesJobs = createTimeSeriesJobData(mlJobService.jobs); let autoZoomDuration: number | undefined; - if (selectedJobIds !== undefined && selectedJobIds.length === 1 && selectedJob !== undefined) { - autoZoomDuration = getAutoZoomDuration( - createTimeSeriesJobData(mlJobService.jobs), - mlJobService.getJob(selectedJobIds[0]) - ); + if (selectedJobId !== undefined && selectedJob !== undefined) { + autoZoomDuration = getAutoZoomDuration(timeSeriesJobs, selectedJob); } const appStateHandler = useCallback( @@ -257,6 +270,18 @@ const TimeSeriesExplorerUrlStateManager: FC = const tzConfig = config.get('dateFormat:tz'); const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + if (timeSeriesJobs.length === 0) { + return ( + + + + ); + } + + if (selectedJobId === undefined || autoZoomDuration === undefined || bounds === undefined) { + return null; + } + return ( = autoZoomDuration, bounds, dateFormatTz, - jobsWithTimeRange, lastRefresh, - selectedJobIds, + selectedJobId, selectedDetectorIndex, selectedEntities, selectedForecastId, diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js deleted file mode 100644 index 1b0151897d1a6a..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/services/job_messages_service.js +++ /dev/null @@ -1,193 +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. - */ - -// Service for carrying out Elasticsearch queries to obtain data for the -// Ml Results dashboards. -import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { ml } from '../services/ml_api_service'; - -// filter to match job_type: 'anomaly_detector' or no job_type field at all -// if no job_type field exist, we can assume the message is for an anomaly detector job -const anomalyDetectorTypeFilter = { - bool: { - should: [ - { - term: { - job_type: 'anomaly_detector', - }, - }, - { - bool: { - must_not: { - exists: { - field: 'job_type', - }, - }, - }, - }, - ], - minimum_should_match: 1, - }, -}; - -// search for audit messages, jobId is optional. -// without it, all jobs will be listed. -// fromRange should be a string formatted in ES time units. e.g. 12h, 1d, 7d -function getJobAuditMessages(fromRange, jobId) { - return new Promise((resolve, reject) => { - let jobFilter = {}; - // if no jobId specified, load all of the messages - if (jobId !== undefined) { - jobFilter = { - bool: { - should: [ - { - term: { - job_id: '', // catch system messages - }, - }, - { - term: { - job_id: jobId, // messages for specified jobId - }, - }, - ], - }, - }; - } - - let timeFilter = {}; - if (fromRange !== undefined && fromRange !== '') { - timeFilter = { - range: { - timestamp: { - gte: `now-${fromRange}`, - lte: 'now', - }, - }, - }; - } - - ml.esSearch({ - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: 1000, - body: { - sort: [{ timestamp: { order: 'asc' } }, { job_id: { order: 'asc' } }], - query: { - bool: { - filter: [ - { - bool: { - must_not: { - term: { - level: 'activity', - }, - }, - }, - }, - anomalyDetectorTypeFilter, - jobFilter, - timeFilter, - ], - }, - }, - }, - }) - .then(resp => { - let messages = []; - if (resp.hits.total !== 0) { - messages = resp.hits.hits.map(hit => hit._source); - } - resolve({ messages }); - }) - .catch(resp => { - reject(resp); - }); - }); -} - -// search highest, most recent audit messages for all jobs for the last 24hrs. -function getAuditMessagesSummary() { - return new Promise((resolve, reject) => { - ml.esSearch({ - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: 0, - body: { - query: { - bool: { - filter: [ - { - range: { - timestamp: { - gte: 'now-1d', - }, - }, - }, - anomalyDetectorTypeFilter, - ], - }, - }, - aggs: { - levelsPerJob: { - terms: { - field: 'job_id', - }, - aggs: { - levels: { - terms: { - field: 'level', - }, - aggs: { - latestMessage: { - terms: { - field: 'message.raw', - size: 1, - order: { - latestMessage: 'desc', - }, - }, - aggs: { - latestMessage: { - max: { - field: 'timestamp', - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }) - .then(resp => { - let messagesPerJob = []; - if ( - resp.hits.total !== 0 && - resp.aggregations && - resp.aggregations.levelsPerJob && - resp.aggregations.levelsPerJob.buckets && - resp.aggregations.levelsPerJob.buckets.length - ) { - messagesPerJob = resp.aggregations.levelsPerJob.buckets; - } - resolve({ messagesPerJob }); - }) - .catch(resp => { - reject(resp); - }); - }); -} - -export const jobMessagesService = { - getJobAuditMessages, - getAuditMessagesSummary, -}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index db9d158c0ead99..6420b60e4c8380 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -22,6 +22,12 @@ import { PartitionFieldsDefinition } from '../results_service/result_service_rx' import { annotations } from './annotations'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { CombinedJob, JobId } from '../../jobs/new_job/common/job_creator/configs'; +import { + CategorizationAnalyzer, + CategoryFieldExample, + FieldExampleCheck, +} from '../../../../common/types/categories'; +import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/new_job'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -184,8 +190,13 @@ declare interface Ml { timeField: string | undefined, start: number, end: number, - analyzer: any - ): Promise<{ valid: number; examples: any[]; sampleSize: number }>; + analyzer: CategorizationAnalyzer + ): Promise<{ + examples: CategoryFieldExample[]; + sampleSize: number; + overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS; + validationChecks: FieldExampleCheck[]; + }>; topCategories( jobId: string, count: number diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts index 6bf5a7b0c97433..304778281c2f2f 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_server_info.ts @@ -5,6 +5,7 @@ */ import { ml } from './ml_api_service'; +import { CategorizationAnalyzer } from '../../../common/types/categories'; export interface MlServerDefaults { anomaly_detectors: { @@ -16,13 +17,6 @@ export interface MlServerDefaults { datafeeds: { scroll_size?: number }; } -export interface CategorizationAnalyzer { - char_filter?: any[]; - tokenizer?: string; - filter?: any[]; - analyzer?: string; -} - export interface MlServerLimits { max_model_memory_limit?: string; } diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap index a4ad3777f4be53..7b59fb0ea61dac 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap @@ -11,6 +11,7 @@ exports[`EventsTable Renders events table with no search bar 1`] = ` Object { "field": "description", "name": "Description", + "scope": "row", "sortable": true, "truncateText": true, }, @@ -79,6 +80,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` Object { "field": "description", "name": "Description", + "scope": "row", "sortable": true, "truncateText": true, }, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js index d5b4207e283ed5..125c75d438af98 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js @@ -63,6 +63,7 @@ export const EventsTable = injectI18n(function EventsTable({ }), sortable: true, truncateText: true, + scope: 'row', }, { field: 'start_time', diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index 8c518f42e0ec77..ff74c592b2b0f6 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -9,6 +9,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` "field": "calendar_id", "name": "ID", "render": [Function], + "scope": "row", "sortable": true, "truncateText": true, }, diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index 45c214aede851f..774cc96517cc65 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -43,6 +43,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ }), sortable: true, truncateText: true, + scope: 'row', render: id => {id}, }, { diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap index 0ad66c78b9e2b7..8985469a807af9 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/__snapshots__/table.test.js.snap @@ -10,6 +10,7 @@ exports[`Filter Lists Table renders with filter lists and selection supplied 1`] "field": "filter_id", "name": "ID", "render": [Function], + "scope": "row", "sortable": true, }, Object { @@ -118,6 +119,7 @@ exports[`Filter Lists Table renders with filter lists supplied 1`] = ` "field": "filter_id", "name": "ID", "render": [Function], + "scope": "row", "sortable": true, }, Object { diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js index 79b71e65c3d1a3..0d1ca66de57756 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -85,6 +85,7 @@ function getColumns() { }), render: id => {id}, sortable: true, + scope: 'row', }, { field: 'description', diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/index.ts diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.js rename to x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index 3edbbc1af23237..6c1bb01137c917 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -4,18 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Timefilter } from 'ui/timefilter'; import { FC } from 'react'; +import { Timefilter } from 'ui/timefilter'; + +import { getDateFormatTz, TimeRangeBounds } from '../explorer/explorer_utils'; + declare const TimeSeriesExplorer: FC<{ appStateHandler: (action: string, payload: any) => void; + autoZoomDuration: number; + bounds: TimeRangeBounds; dateFormatTz: string; - selectedJobIds: string[]; + lastRefresh: number; + selectedJobId: string; selectedDetectorIndex: number; selectedEntities: any[]; selectedForecastId: string; setGlobalState: (arg: any) => void; tableInterval: string; tableSeverity: number; - timefilter: Timefilter; + zoom?: { from: string; to: string }; }>; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 016f054430fa3c..f3d8692bfb3e97 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,7 +8,7 @@ * React component for rendering Single Metric Viewer. */ -import { debounce, difference, each, find, get, has, isEqual, without } from 'lodash'; +import { debounce, each, find, get, has, isEqual } from 'lodash'; import moment from 'moment-timezone'; import { Subject, Subscription, forkJoin } from 'rxjs'; import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; @@ -24,7 +24,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiProgress, EuiSelect, EuiSpacer, EuiText, @@ -49,15 +48,12 @@ import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; import { ChartTooltip } from '../components/chart_tooltip'; import { EntityControl } from './components/entity_control'; import { ForecastingModal } from './components/forecasting_modal/forecasting_modal'; -import { JobSelector } from '../components/job_selector'; -import { getTimeRangeFromSelection } from '../components/job_selector/job_select_service_utils'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; -import { NavigationMenu } from '../components/navigation_menu'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; import { TimeseriesChart } from './components/timeseries_chart/timeseries_chart'; -import { TimeseriesexplorerNoJobsFound } from './components/timeseriesexplorer_no_jobs_found'; import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_no_chart_data'; +import { TimeSeriesExplorerPage } from './timeseriesexplorer_page'; import { ml } from '../services/ml_api_service'; import { mlFieldFormatService } from '../services/field_format_service'; @@ -154,49 +150,23 @@ function getTimeseriesexplorerDefaultState() { }; } -const TimeSeriesExplorerPage = ({ children, jobSelectorProps, loading, resizeRef }) => ( - - - {/* Show animated progress bar while loading */} - {loading && } - {/* Show a progress bar with progress 0% when not loading. - If we'd just show no progress bar when not loading it would result in a flickering height effect. */} - {!loading && ( - - )} - -
- {children} -
-
-); - const containerPadding = 24; export class TimeSeriesExplorer extends React.Component { static propTypes = { appStateHandler: PropTypes.func.isRequired, - autoZoomDuration: PropTypes.number, - bounds: PropTypes.object, + autoZoomDuration: PropTypes.number.isRequired, + bounds: PropTypes.object.isRequired, dateFormatTz: PropTypes.string.isRequired, - jobsWithTimeRange: PropTypes.array.isRequired, lastRefresh: PropTypes.number.isRequired, - selectedJobIds: PropTypes.arrayOf(PropTypes.string), + selectedJobId: PropTypes.string.isRequired, selectedDetectorIndex: PropTypes.number, selectedEntities: PropTypes.object, selectedForecastId: PropTypes.string, + setGlobalState: PropTypes.func.isRequired, tableInterval: PropTypes.string, tableSeverity: PropTypes.number, + zoom: PropTypes.object, }; state = getTimeseriesexplorerDefaultState(); @@ -283,9 +253,9 @@ export class TimeSeriesExplorer extends React.Component { contextChartSelectedInitCallDone = false; getFocusAggregationInterval(selection) { - const { selectedJobIds } = this.props; + const { selectedJobId } = this.props; const jobs = createTimeSeriesJobData(mlJobService.jobs); - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const selectedJob = mlJobService.getJob(selectedJobId); // Calculate the aggregation interval for the focus chart. const bounds = { min: moment(selection.from), max: moment(selection.to) }; @@ -297,9 +267,9 @@ export class TimeSeriesExplorer extends React.Component { * Gets focus data for the current component state/ */ getFocusData(selection) { - const { selectedJobIds, selectedForecastId, selectedDetectorIndex } = this.props; + const { selectedJobId, selectedForecastId, selectedDetectorIndex } = this.props; const { modelPlotEnabled } = this.state; - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const selectedJob = mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); // Calculate the aggregation interval for the focus chart. @@ -354,11 +324,11 @@ export class TimeSeriesExplorer extends React.Component { const { dateFormatTz, selectedDetectorIndex, - selectedJobIds, + selectedJobId, tableInterval, tableSeverity, } = this.props; - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const selectedJob = mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); return ml.results @@ -422,8 +392,8 @@ export class TimeSeriesExplorer extends React.Component { loadEntityValues = async (entities, searchTerm = {}) => { this.setState({ entitiesLoading: true }); - const { bounds, selectedJobIds, selectedDetectorIndex } = this.props; - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const { bounds, selectedJobId, selectedDetectorIndex } = this.props; + const selectedJob = mlJobService.getJob(selectedJobId); // Populate the entity input datalists with the values from the top records by score // for the selected detector across the full time range. No need to pass through finish(). @@ -477,17 +447,13 @@ export class TimeSeriesExplorer extends React.Component { bounds, selectedDetectorIndex, selectedForecastId, - selectedJobIds, + selectedJobId, zoom, } = this.props; - if (selectedJobIds === undefined) { - return; - } - const { loadCounter: currentLoadCounter } = this.state; - const currentSelectedJob = mlJobService.getJob(selectedJobIds[0]); + const currentSelectedJob = mlJobService.getJob(selectedJobId); if (currentSelectedJob === undefined) { return; @@ -524,7 +490,7 @@ export class TimeSeriesExplorer extends React.Component { const { loadCounter, modelPlotEnabled } = this.state; const jobs = createTimeSeriesJobData(mlJobService.jobs); - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const selectedJob = mlJobService.getJob(selectedJobId); const detectorIndex = selectedDetectorIndex; let awaitingCount = 3; @@ -715,8 +681,8 @@ export class TimeSeriesExplorer extends React.Component { * @param callback to invoke after a state update. */ getControlsForDetector = () => { - const { selectedDetectorIndex, selectedEntities, selectedJobIds } = this.props; - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props; + const selectedJob = mlJobService.getJob(selectedJobId); const entities = []; @@ -869,9 +835,9 @@ export class TimeSeriesExplorer extends React.Component { } }), switchMap(selection => { - const { selectedJobIds } = this.props; + const { selectedJobId } = this.props; const jobs = createTimeSeriesJobData(mlJobService.jobs); - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const selectedJob = mlJobService.getJob(selectedJobId); // Calculate the aggregation interval for the focus chart. const bounds = { min: moment(selection.from), max: moment(selection.to) }; @@ -925,133 +891,19 @@ export class TimeSeriesExplorer extends React.Component { this.componentDidUpdate(); } - /** - * returns true/false if setGlobalState has been triggered - * or returns the job id which should be loaded. - */ - checkJobSelection() { - const { jobsWithTimeRange, selectedJobIds, setGlobalState } = this.props; - - const jobs = createTimeSeriesJobData(mlJobService.jobs); - const timeSeriesJobIds = jobs.map(j => j.id); - - // Check if any of the jobs set in the URL are not time series jobs - // (e.g. if switching to this view straight from the Anomaly Explorer). - const invalidIds = difference(selectedJobIds, timeSeriesJobIds); - const validSelectedJobIds = without(selectedJobIds, ...invalidIds); - if (invalidIds.length > 0) { - let warningText = i18n.translate( - 'xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage', - { - defaultMessage: `You can't view requested {invalidIdsCount, plural, one {job} other {jobs}} {invalidIds} in this dashboard`, - values: { - invalidIdsCount: invalidIds.length, - invalidIds, - }, - } - ); - if (validSelectedJobIds.length === 0 && timeSeriesJobIds.length > 0) { - warningText += i18n.translate('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', { - defaultMessage: ', auto selecting first job', - }); - } - toastNotifications.addWarning(warningText); - } - - if (validSelectedJobIds.length > 1) { - // if more than one job or a group has been loaded from the URL - if (validSelectedJobIds.length > 1) { - // if more than one job, select the first job from the selection. - toastNotifications.addWarning( - i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { - defaultMessage: 'You can only view one job at a time in this dashboard', - }) - ); - setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] }); - return true; - } else { - // if a group has been loaded - if (selectedJobIds.length > 0) { - // if the group contains valid jobs, select the first - toastNotifications.addWarning( - i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { - defaultMessage: 'You can only view one job at a time in this dashboard', - }) - ); - setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] }); - return true; - } else if (jobs.length > 0) { - // if there are no valid jobs in the group but there are valid jobs - // in the list of all jobs, select the first - const jobIds = [jobs[0].id]; - const time = getTimeRangeFromSelection(jobsWithTimeRange, jobIds); - setGlobalState({ - ...{ ml: { jobIds } }, - ...(time !== undefined ? { time } : {}), - }); - return true; - } else { - // if there are no valid jobs left. - return false; - } - } - } else if (invalidIds.length > 0 && validSelectedJobIds.length > 0) { - // if some ids have been filtered out because they were invalid. - // refresh the URL with the first valid id - setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] }); - return true; - } else if (validSelectedJobIds.length > 0) { - // normal behavior. a job ID has been loaded from the URL - // Clear the detectorIndex, entities and forecast info. - return validSelectedJobIds[0]; - } else { - if (validSelectedJobIds.length === 0 && jobs.length > 0) { - // no jobs were loaded from the URL, so add the first job - // from the full jobs list. - const jobIds = [jobs[0].id]; - const time = getTimeRangeFromSelection(jobsWithTimeRange, jobIds); - setGlobalState({ - ...{ ml: { jobIds } }, - ...(time !== undefined ? { time } : {}), - }); - return true; - } else { - // Jobs exist, but no time series jobs. - return false; - } - } - } - componentDidUpdate(previousProps) { - if ( - previousProps === undefined || - !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds) - ) { - const update = this.checkJobSelection(); - // - true means a setGlobalState got triggered and - // we'll just wait for the next React render. - // - false means there are either no jobs or no time based jobs present. - // - if we get back a string it means we got back a job id we can load. - if (update === true) { - return; - } else if (update === false) { - this.setState({ loading: false }); - return; - } else if (typeof update === 'string') { - this.contextChartSelectedInitCallDone = false; - this.setState({ fullRefresh: false, loading: true }, () => { - this.loadForJobId(update); - }); - } + if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) { + this.contextChartSelectedInitCallDone = false; + this.setState({ fullRefresh: false, loading: true }, () => { + this.loadForJobId(this.props.selectedJobId); + }); } if ( - this.props.bounds !== undefined && - this.props.selectedJobIds !== undefined && - (previousProps === undefined || - !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds) || - previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex || - !isEqual(previousProps.selectedEntities, this.props.selectedEntities)) + previousProps === undefined || + previousProps.selectedJobId !== this.props.selectedJobId || + previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex || + !isEqual(previousProps.selectedEntities, this.props.selectedEntities) ) { const entityControls = this.getControlsForDetector(); this.loadEntityValues(entityControls); @@ -1074,7 +926,7 @@ export class TimeSeriesExplorer extends React.Component { !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || !isEqual(previousProps.selectedForecastId, this.props.selectedForecastId) || - !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds) || + previousProps.selectedJobId !== this.props.selectedJobId || !isEqual(previousProps.zoom, this.props.zoom) ) { const fullRefresh = @@ -1084,7 +936,7 @@ export class TimeSeriesExplorer extends React.Component { !isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) || !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || !isEqual(previousProps.selectedForecastId, this.props.selectedForecastId) || - !isEqual(previousProps.selectedJobIds, this.props.selectedJobIds); + previousProps.selectedJobId !== this.props.selectedJobId; this.loadSingleMetricData(fullRefresh); } @@ -1157,7 +1009,7 @@ export class TimeSeriesExplorer extends React.Component { dateFormatTz, lastRefresh, selectedDetectorIndex, - selectedJobIds, + selectedJobId, } = this.props; const { @@ -1209,34 +1061,13 @@ export class TimeSeriesExplorer extends React.Component { autoZoomDuration, }; - const jobSelectorProps = { - dateFormatTz, - singleSelection: true, - timeseriesOnly: true, - }; - const jobs = createTimeSeriesJobData(mlJobService.jobs); - if (jobs.length === 0) { - return ( - - - - ); - } - - if ( - selectedJobIds === undefined || - selectedJobIds.length > 1 || - selectedDetectorIndex === undefined || - mlJobService.getJob(selectedJobIds[0]) === undefined - ) { - return ( - - ); + if (selectedDetectorIndex === undefined || mlJobService.getJob(selectedJobId) === undefined) { + return ; } - const selectedJob = mlJobService.getJob(selectedJobIds[0]); + const selectedJob = mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); const fieldNamesWithEmptyValues = entityControls @@ -1278,7 +1109,7 @@ export class TimeSeriesExplorer extends React.Component { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx new file mode 100644 index 00000000000000..9da1a79232fce5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_page.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiProgress } from '@elastic/eui'; + +import { JobSelector } from '../components/job_selector'; +import { NavigationMenu } from '../components/navigation_menu'; + +interface TimeSeriesExplorerPageProps { + dateFormatTz: string; + loading?: boolean; + resizeRef?: any; +} + +export const TimeSeriesExplorerPage: FC = ({ + children, + dateFormatTz, + loading, + resizeRef, +}) => { + return ( + <> + + {/* Show animated progress bar while loading */} + {loading === true && ( + + )} + {/* Show a progress bar with progress 0% when not loading. + If we'd just show no progress bar when not loading it would result in a flickering height effect. */} + {loading === false && ( + + )} + +
+ {children} +
+ + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts index 578dbdf1277a05..dcfbe94c97cc6e 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/index.ts @@ -6,3 +6,4 @@ export { getFocusData } from './get_focus_data'; export * from './timeseriesexplorer_utils'; +export { validateJobSelection } from './validate_job_selection'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts new file mode 100644 index 00000000000000..f1cdaf3ba8c1bc --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.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 { difference, without } from 'lodash'; + +import { i18n } from '@kbn/i18n'; + +import { toastNotifications } from 'ui/notify'; + +import { MlJobWithTimeRange } from '../../../../common/types/jobs'; + +import { getTimeRangeFromSelection } from '../../components/job_selector/job_select_service_utils'; +import { mlJobService } from '../../services/job_service'; + +import { createTimeSeriesJobData } from './timeseriesexplorer_utils'; + +/** + * returns true/false if setGlobalState has been triggered + * or returns the job id which should be loaded. + */ +export function validateJobSelection( + jobsWithTimeRange: MlJobWithTimeRange[], + selectedJobIds: string[], + setGlobalState: (...args: any) => void +) { + const jobs = createTimeSeriesJobData(mlJobService.jobs); + const timeSeriesJobIds: string[] = jobs.map((j: any) => j.id); + + // Check if any of the jobs set in the URL are not time series jobs + // (e.g. if switching to this view straight from the Anomaly Explorer). + const invalidIds: string[] = difference(selectedJobIds, timeSeriesJobIds); + const validSelectedJobIds = without(selectedJobIds, ...invalidIds); + if (invalidIds.length > 0) { + let warningText = i18n.translate( + 'xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage', + { + defaultMessage: `You can't view requested {invalidIdsCount, plural, one {job} other {jobs}} {invalidIds} in this dashboard`, + values: { + invalidIdsCount: invalidIds.length, + invalidIds: invalidIds.join(', '), + }, + } + ); + if (validSelectedJobIds.length === 0 && timeSeriesJobIds.length > 0) { + warningText += i18n.translate('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', { + defaultMessage: ', auto selecting first job', + }); + } + toastNotifications.addWarning(warningText); + } + + if (validSelectedJobIds.length > 1) { + // if more than one job, select the first job from the selection. + toastNotifications.addWarning( + i18n.translate('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', { + defaultMessage: 'You can only view one job at a time in this dashboard', + }) + ); + setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] }); + return true; + } else if (invalidIds.length > 0 && validSelectedJobIds.length > 0) { + // if some ids have been filtered out because they were invalid. + // refresh the URL with the first valid id + setGlobalState('ml', { jobIds: [validSelectedJobIds[0]] }); + return true; + } else if (validSelectedJobIds.length === 1) { + // normal behavior. a job ID has been loaded from the URL + // Clear the detectorIndex, entities and forecast info. + return validSelectedJobIds[0]; + } else if (validSelectedJobIds.length === 0 && jobs.length > 0) { + // no jobs were loaded from the URL, so add the first job + // from the full jobs list. + const jobIds = [jobs[0].id]; + const time = getTimeRangeFromSelection(jobsWithTimeRange, jobIds); + setGlobalState({ + ...{ ml: { jobIds } }, + ...(time !== undefined ? { time } : {}), + }); + return true; + } else { + // Jobs exist, but no time series jobs. + return false; + } +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index c9cc8a3da574ad..52495b3b732d0a 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -10,6 +10,30 @@ import moment from 'moment'; const SIZE = 1000; const LEVEL = { system_info: -1, info: 0, warning: 1, error: 2 }; +// filter to match job_type: 'anomaly_detector' or no job_type field at all +// if no job_type field exist, we can assume the message is for an anomaly detector job +const anomalyDetectorTypeFilter = { + bool: { + should: [ + { + term: { + job_type: 'anomaly_detector', + }, + }, + { + bool: { + must_not: { + exists: { + field: 'job_type', + }, + }, + }, + }, + ], + minimum_should_match: 1, + }, +}; + export function jobAuditMessagesProvider(callWithRequest) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. @@ -47,13 +71,9 @@ export function jobAuditMessagesProvider(callWithRequest) { level: 'activity', }, }, - must: { - term: { - job_type: 'anomaly_detector', - }, - }, }, }, + anomalyDetectorTypeFilter, timeFilter, ], }, @@ -119,6 +139,7 @@ export function jobAuditMessagesProvider(callWithRequest) { }, }, }, + anomalyDetectorTypeFilter, ], }, }; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 186bcbae84546f..5c0eff3112a53e 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -8,7 +8,11 @@ import { datafeedsProvider } from './datafeeds'; import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; import { newJobCapsProvider } from './new_job_caps'; -import { newJobChartsProvider, categorizationExamplesProvider } from './new_job'; +import { + newJobChartsProvider, + categorizationExamplesProvider, + topCategoriesProvider, +} from './new_job'; export function jobServiceProvider(callWithRequest, request) { return { @@ -18,5 +22,6 @@ export function jobServiceProvider(callWithRequest, request) { ...newJobCapsProvider(callWithRequest, request), ...newJobChartsProvider(callWithRequest, request), ...categorizationExamplesProvider(callWithRequest, request), + ...topCategoriesProvider(callWithRequest, request), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts deleted file mode 100644 index b3c70bf589cd04..00000000000000 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization.ts +++ /dev/null @@ -1,314 +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 { chunk } from 'lodash'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; -import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../common/constants/new_job'; -import { CategoryId, Category, Token } from '../../../../common/types/categories'; -import { callWithRequestType } from '../../../../common/types/kibana'; - -const VALID_TOKEN_COUNT = 3; -const CHUNK_SIZE = 100; - -export function categorizationExamplesProvider(callWithRequest: callWithRequestType) { - async function categorizationExamples( - indexPatternTitle: string, - query: any, - size: number, - categorizationFieldName: string, - timeField: string | undefined, - start: number, - end: number, - analyzer?: any - ) { - if (timeField !== undefined) { - const range = { - range: { - [timeField]: { - gte: start, - format: 'epoch_millis', - }, - }, - }; - - if (query.bool === undefined) { - query.bool = {}; - } - if (query.bool.filter === undefined) { - query.bool.filter = range; - } else { - if (Array.isArray(query.bool.filter)) { - query.bool.filter.push(range); - } else { - query.bool.filter.range = range; - } - } - } - - const results = await callWithRequest('search', { - index: indexPatternTitle, - size, - body: { - _source: categorizationFieldName, - query, - }, - }); - const examples: string[] = results.hits?.hits - ?.map((doc: any) => doc._source[categorizationFieldName]) - .filter((example: string | null | undefined) => example !== undefined && example !== null); - - async function loadTokens(chunkSize: number) { - const exampleChunks = chunk(examples, chunkSize); - const tokensPerChunks = await Promise.all(exampleChunks.map(c => getTokens(c, analyzer))); - const tokensPerExample = tokensPerChunks.flat(); - return examples.map((e, i) => ({ text: e, tokens: tokensPerExample[i] })); - } - try { - return loadTokens(CHUNK_SIZE); - } catch (error) { - // if an error is thrown when loading the tokens, lower the chunk size by half and try again - // the error may have been caused by too many tokens being found. - // the _analyze endpoint has a maximum of 10000 tokens. - return loadTokens(CHUNK_SIZE / 2); - } - } - - async function getTokens(examples: string[], analyzer?: any) { - const { tokens }: { tokens: Token[] } = await callWithRequest('indices.analyze', { - body: { - ...getAnalyzer(analyzer), - text: examples, - }, - }); - - const lengths = examples.map(e => e.length); - const sumLengths = lengths.map((s => (a: number) => (s += a))(0)); - - const tokensPerExample: Token[][] = examples.map(e => []); - - tokens.forEach((t, i) => { - for (let g = 0; g < sumLengths.length; g++) { - if (t.start_offset <= sumLengths[g] + g) { - const offset = g > 0 ? sumLengths[g - 1] + g : 0; - tokensPerExample[g].push({ - ...t, - start_offset: t.start_offset - offset, - end_offset: t.end_offset - offset, - }); - break; - } - } - }); - return tokensPerExample; - } - - function getAnalyzer(analyzer: any) { - if (typeof analyzer === 'object' && analyzer.tokenizer !== undefined) { - return analyzer; - } else { - return { analyzer: 'standard' }; - } - } - - async function validateCategoryExamples( - indexPatternTitle: string, - query: any, - size: number, - categorizationFieldName: string, - timeField: string | undefined, - start: number, - end: number, - analyzer?: any - ) { - const resp = await categorizationExamples( - indexPatternTitle, - query, - CATEGORY_EXAMPLES_SAMPLE_SIZE, - categorizationFieldName, - timeField, - start, - end, - analyzer - ); - - const sortedExamples = resp - .map((e, i) => ({ ...e, origIndex: i })) - .sort((a, b) => b.tokens.length - a.tokens.length); - const validExamples = sortedExamples.filter(e => e.tokens.length >= VALID_TOKEN_COUNT); - const sampleSize = sortedExamples.length; - - const multiple = Math.floor(sampleSize / size) || sampleSize; - const filteredExamples = []; - let i = 0; - while (filteredExamples.length < size && i < sortedExamples.length) { - filteredExamples.push(sortedExamples[i]); - i += multiple; - } - const examples = filteredExamples - .sort((a, b) => a.origIndex - b.origIndex) - .map(e => ({ text: e.text, tokens: e.tokens })); - - return { - sampleSize, - valid: sortedExamples.length === 0 ? 0 : validExamples.length / sortedExamples.length, - examples, - }; - } - - async function getTotalCategories(jobId: string): Promise<{ total: number }> { - const totalResp = await callWithRequest('search', { - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - term: { - job_id: jobId, - }, - }, - { - exists: { - field: 'category_id', - }, - }, - ], - }, - }, - }, - }); - return totalResp?.hits?.total?.value ?? 0; - } - - async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { - const top = await callWithRequest('search', { - index: ML_RESULTS_INDEX_PATTERN, - size: 0, - body: { - query: { - bool: { - filter: [ - { - term: { - job_id: jobId, - }, - }, - { - term: { - result_type: 'model_plot', - }, - }, - { - term: { - by_field_name: 'mlcategory', - }, - }, - ], - }, - }, - aggs: { - cat_count: { - terms: { - field: 'by_field_value', - size: numberOfCategories, - }, - }, - }, - }, - }); - - const catCounts: Array<{ - id: CategoryId; - count: number; - }> = top.aggregations?.cat_count?.buckets.map((c: any) => ({ - id: c.key, - count: c.doc_count, - })); - return catCounts || []; - } - - async function getCategories( - jobId: string, - catIds: CategoryId[], - size: number - ): Promise { - const categoryFilter = catIds.length - ? { - terms: { - category_id: catIds, - }, - } - : { - exists: { - field: 'category_id', - }, - }; - const result = await callWithRequest('search', { - index: ML_RESULTS_INDEX_PATTERN, - size, - body: { - query: { - bool: { - filter: [ - { - term: { - job_id: jobId, - }, - }, - categoryFilter, - ], - }, - }, - }, - }); - - return result.hits.hits?.map((c: { _source: Category }) => c._source) || []; - } - - async function topCategories(jobId: string, numberOfCategories: number) { - const catCounts = await getTopCategoryCounts(jobId, numberOfCategories); - const categories = await getCategories( - jobId, - catCounts.map(c => c.id), - catCounts.length || numberOfCategories - ); - - const catsById = categories.reduce((p, c) => { - p[c.category_id] = c; - return p; - }, {} as { [id: number]: Category }); - - const total = await getTotalCategories(jobId); - - if (catCounts.length) { - return { - total, - categories: catCounts.map(({ id, count }) => { - return { - count, - category: catsById[id] ?? null, - }; - }), - }; - } else { - return { - total, - categories: categories.map(category => { - return { - category, - }; - }), - }; - } - } - - return { - categorizationExamples, - validateCategoryExamples, - topCategories, - }; -} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts new file mode 100644 index 00000000000000..76473bd55db7fb --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { chunk } from 'lodash'; +import { SearchResponse } from 'elasticsearch'; +import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/new_job'; +import { + Token, + CategorizationAnalyzer, + CategoryFieldExample, +} from '../../../../../common/types/categories'; +import { callWithRequestType } from '../../../../../common/types/kibana'; +import { ValidationResults } from './validation_results'; + +const CHUNK_SIZE = 100; + +export function categorizationExamplesProvider(callWithRequest: callWithRequestType) { + const validationResults = new ValidationResults(); + + async function categorizationExamples( + indexPatternTitle: string, + query: any, + size: number, + categorizationFieldName: string, + timeField: string | undefined, + start: number, + end: number, + analyzer: CategorizationAnalyzer + ): Promise<{ examples: CategoryFieldExample[]; error?: any }> { + if (timeField !== undefined) { + const range = { + range: { + [timeField]: { + gte: start, + lt: end, + format: 'epoch_millis', + }, + }, + }; + if (query.bool === undefined) { + query.bool = {}; + } + if (query.bool.filter === undefined) { + query.bool.filter = range; + } else { + if (Array.isArray(query.bool.filter)) { + query.bool.filter.push(range); + } else { + query.bool.filter.range = range; + } + } + } + + const results: SearchResponse<{ [id: string]: string }> = await callWithRequest('search', { + index: indexPatternTitle, + size, + body: { + _source: categorizationFieldName, + query, + sort: ['_doc'], + }, + }); + + const tempExamples = results.hits.hits.map(({ _source }) => _source[categorizationFieldName]); + + validationResults.createNullValueResult(tempExamples); + + const allExamples = tempExamples.filter( + (example: string | null | undefined) => example !== undefined && example !== null + ); + + validationResults.createMedianMessageLengthResult(allExamples); + + try { + const examplesWithTokens = await getTokens(CHUNK_SIZE, allExamples, analyzer); + return { examples: examplesWithTokens }; + } catch (err) { + // console.log('dropping to 50 chunk size'); + // if an error is thrown when loading the tokens, lower the chunk size by half and try again + // the error may have been caused by too many tokens being found. + // the _analyze endpoint has a maximum of 10000 tokens. + const halfExamples = allExamples.splice(0, Math.ceil(allExamples.length / 2)); + const halfChunkSize = CHUNK_SIZE / 2; + try { + const examplesWithTokens = await getTokens(halfChunkSize, halfExamples, analyzer); + return { examples: examplesWithTokens }; + } catch (error) { + validationResults.createTooManyTokensResult(error, halfChunkSize); + return { examples: halfExamples.map(e => ({ text: e, tokens: [] })) }; + } + } + } + + async function getTokens( + chunkSize: number, + examples: string[], + analyzer: CategorizationAnalyzer + ): Promise { + const exampleChunks = chunk(examples, chunkSize); + const tokensPerExampleChunks: Token[][][] = []; + for (const c of exampleChunks) { + tokensPerExampleChunks.push(await loadTokens(c, analyzer)); + } + const tokensPerExample = tokensPerExampleChunks.flat(); + return examples.map((e, i) => ({ text: e, tokens: tokensPerExample[i] })); + } + + async function loadTokens(examples: string[], analyzer: CategorizationAnalyzer) { + const { tokens }: { tokens: Token[] } = await callWithRequest('indices.analyze', { + body: { + ...getAnalyzer(analyzer), + text: examples, + }, + }); + + const lengths = examples.map(e => e.length); + const sumLengths = lengths.map((s => (a: number) => (s += a))(0)); + + const tokensPerExample: Token[][] = examples.map(e => []); + + tokens.forEach((t, i) => { + for (let g = 0; g < sumLengths.length; g++) { + if (t.start_offset <= sumLengths[g] + g) { + const offset = g > 0 ? sumLengths[g - 1] + g : 0; + tokensPerExample[g].push({ + ...t, + start_offset: t.start_offset - offset, + end_offset: t.end_offset - offset, + }); + break; + } + } + }); + return tokensPerExample; + } + + function getAnalyzer(analyzer: CategorizationAnalyzer) { + if (typeof analyzer === 'object' && analyzer.tokenizer !== undefined) { + return analyzer; + } else { + return { analyzer: 'standard' }; + } + } + + async function validateCategoryExamples( + indexPatternTitle: string, + query: any, + size: number, + categorizationFieldName: string, + timeField: string | undefined, + start: number, + end: number, + analyzer: CategorizationAnalyzer + ) { + const resp = await categorizationExamples( + indexPatternTitle, + query, + CATEGORY_EXAMPLES_SAMPLE_SIZE, + categorizationFieldName, + timeField, + start, + end, + analyzer + ); + + const { examples } = resp; + const sampleSize = examples.length; + validationResults.createTokenCountResult(examples, sampleSize); + + // sort examples by number of tokens, keeping track of their original order + // with an origIndex property + const sortedExamples = examples + .map((e, i) => ({ ...e, origIndex: i })) + .sort((a, b) => b.tokens.length - a.tokens.length); + + // we only want 'size' (e.g. 5) number of examples, + // so loop through the sorted examples, taking 5 at evenly + // spread intervals + const multiple = Math.floor(sampleSize / size) || sampleSize; + const filteredExamples = []; + let i = 0; + while (filteredExamples.length < size && i < sampleSize) { + filteredExamples.push(sortedExamples[i]); + i += multiple; + } + + // sort back into original order and remove origIndex property + const processedExamples = filteredExamples + .sort((a, b) => a.origIndex - b.origIndex) + .map(e => ({ text: e.text, tokens: e.tokens })); + + return { + overallValidStatus: validationResults.overallResult, + validationChecks: validationResults.results, + sampleSize, + examples: processedExamples, + }; + } + + return { + validateCategoryExamples, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts new file mode 100644 index 00000000000000..be32b99b5e527e --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { categorizationExamplesProvider } from './examples'; +export { topCategoriesProvider } from './top_categories'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts new file mode 100644 index 00000000000000..3361cc454e2b7b --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { SearchResponse } from 'elasticsearch'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; +import { CategoryId, Category } from '../../../../../common/types/categories'; +import { callWithRequestType } from '../../../../../common/types/kibana'; + +export function topCategoriesProvider(callWithRequest: callWithRequestType) { + async function getTotalCategories(jobId: string): Promise<{ total: number }> { + const totalResp = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + term: { + job_id: jobId, + }, + }, + { + exists: { + field: 'category_id', + }, + }, + ], + }, + }, + }, + }); + return totalResp?.hits?.total?.value ?? 0; + } + + async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { + const top: SearchResponse = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size: 0, + body: { + query: { + bool: { + filter: [ + { + term: { + job_id: jobId, + }, + }, + { + term: { + result_type: 'model_plot', + }, + }, + { + term: { + by_field_name: 'mlcategory', + }, + }, + ], + }, + }, + aggs: { + cat_count: { + terms: { + field: 'by_field_value', + size: numberOfCategories, + }, + }, + }, + }, + }); + + const catCounts: Array<{ + id: CategoryId; + count: number; + }> = top.aggregations?.cat_count?.buckets.map((c: any) => ({ + id: c.key, + count: c.doc_count, + })); + return catCounts || []; + } + + async function getCategories( + jobId: string, + catIds: CategoryId[], + size: number + ): Promise { + const categoryFilter = catIds.length + ? { + terms: { + category_id: catIds, + }, + } + : { + exists: { + field: 'category_id', + }, + }; + const result: SearchResponse = await callWithRequest('search', { + index: ML_RESULTS_INDEX_PATTERN, + size, + body: { + query: { + bool: { + filter: [ + { + term: { + job_id: jobId, + }, + }, + categoryFilter, + ], + }, + }, + }, + }); + + return result.hits.hits?.map((c: { _source: Category }) => c._source) || []; + } + + async function topCategories(jobId: string, numberOfCategories: number) { + const catCounts = await getTopCategoryCounts(jobId, numberOfCategories); + const categories = await getCategories( + jobId, + catCounts.map(c => c.id), + catCounts.length || numberOfCategories + ); + + const catsById = categories.reduce((p, c) => { + p[c.category_id] = c; + return p; + }, {} as { [id: number]: Category }); + + const total = await getTotalCategories(jobId); + + if (catCounts.length) { + return { + total, + categories: catCounts.map(({ id, count }) => { + return { + count, + category: catsById[id] ?? null, + }; + }), + }; + } else { + return { + total, + categories: categories.map(category => { + return { + category, + }; + }), + }; + } + } + + return { + topCategories, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts new file mode 100644 index 00000000000000..e173b893dfbfa1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + CATEGORY_EXAMPLES_VALIDATION_STATUS, + CATEGORY_EXAMPLES_ERROR_LIMIT, + CATEGORY_EXAMPLES_WARNING_LIMIT, +} from '../../../../../common/constants/new_job'; +import { + FieldExampleCheck, + CategoryFieldExample, + VALIDATION_RESULT, +} from '../../../../../common/types/categories'; +import { getMedianStringLength } from '../../../../../common/util/string_utils'; + +const VALID_TOKEN_COUNT = 3; +const MEDIAN_LINE_LENGTH_LIMIT = 400; +const NULL_COUNT_PERCENT_LIMIT = 0.75; + +export class ValidationResults { + private _results: FieldExampleCheck[] = []; + + public get results() { + return this._results; + } + + public get overallResult() { + if (this._results.some(c => c.valid === CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID)) { + return CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; + } + if (this._results.some(c => c.valid === CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID)) { + return CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID; + } + return CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID; + } + + private _resultExists(id: VALIDATION_RESULT) { + return this._results.some(r => r.id === id); + } + + public createTokenCountResult(examples: CategoryFieldExample[], sampleSize: number) { + if (examples.length === 0) { + this.createNoExamplesResult(); + return; + } + + if (this._resultExists(VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES) === true) { + // if tokenizing has failed due to insufficient privileges, don't show + // the message about token count + return; + } + + const validExamplesSize = examples.filter(e => e.tokens.length >= VALID_TOKEN_COUNT).length; + const percentValid = sampleSize === 0 ? 0 : validExamplesSize / sampleSize; + + let valid = CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID; + if (percentValid < CATEGORY_EXAMPLES_ERROR_LIMIT) { + valid = CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID; + } else if (percentValid < CATEGORY_EXAMPLES_WARNING_LIMIT) { + valid = CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID; + } + + const message = i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.tokenLengthValidation', + { + defaultMessage: + '{number} field {number, plural, zero {value} one {value} other {values}} analyzed, {percentage}% contain {validTokenCount} or more tokens.', + values: { + number: sampleSize, + percentage: Math.floor(percentValid * 100), + validTokenCount: VALID_TOKEN_COUNT, + }, + } + ); + + if ( + this._resultExists(VALIDATION_RESULT.TOO_MANY_TOKENS) === false && + this._resultExists(VALIDATION_RESULT.FAILED_TO_TOKENIZE) === false + ) { + this._results.unshift({ + id: VALIDATION_RESULT.TOKEN_COUNT, + valid, + message, + }); + } + } + + public createMedianMessageLengthResult(examples: string[]) { + const median = getMedianStringLength(examples); + + if (median > MEDIAN_LINE_LENGTH_LIMIT) { + this._results.push({ + id: VALIDATION_RESULT.MEDIAN_LINE_LENGTH, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.medianLineLength', + { + defaultMessage: + 'The median length for the field values analyzed is over {medianLimit} characters.', + values: { medianLimit: MEDIAN_LINE_LENGTH_LIMIT }, + } + ), + }); + } + } + + public createNoExamplesResult() { + this._results.push({ + id: VALIDATION_RESULT.NULL_VALUES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate('xpack.ml.models.jobService.categorization.messages.noDataFound', { + defaultMessage: + 'No examples for this field could be found. Please ensure the selected date range contains data.', + }), + }); + } + + public createNullValueResult(examples: Array) { + const nullCount = examples.filter(e => e === null).length; + + if (nullCount / examples.length >= NULL_COUNT_PERCENT_LIMIT) { + this._results.push({ + id: VALIDATION_RESULT.NULL_VALUES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate('xpack.ml.models.jobService.categorization.messages.nullValues', { + defaultMessage: 'More than {percent}% of field values are null.', + values: { percent: NULL_COUNT_PERCENT_LIMIT * 100 }, + }), + }); + } + } + + public createTooManyTokensResult(error: any, sampleSize: number) { + // expecting error message: + // The number of tokens produced by calling _analyze has exceeded the allowed maximum of [10000]. + // This limit can be set by changing the [index.analyze.max_token_count] index level setting. + + if (error.statusCode === 403) { + this.createPrivilegesErrorResult(error); + return; + } + const message: string = error.message; + if (message) { + const rxp = /exceeded the allowed maximum of \[(\d+?)\]/; + const match = rxp.exec(message); + if (match?.length === 2) { + const tokenLimit = match[1]; + this._results.push({ + id: VALIDATION_RESULT.TOO_MANY_TOKENS, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.tooManyTokens', + { + defaultMessage: + 'Tokenization of field value examples has failed due to more than {tokenLimit} tokens being found in a sample of {sampleSize} values.', + values: { sampleSize, tokenLimit }, + } + ), + }); + return; + } + return; + } + this.createFailureToTokenize(message); + } + + public createPrivilegesErrorResult(error: any) { + const message: string = error.message; + if (message) { + this._results.push({ + id: VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.insufficientPrivileges', + { + defaultMessage: + 'Tokenization of field value examples could not be performed due to insufficient privileges. Field values cannot therefore be checked to see if they are appropriate for use in a categorization job.', + } + ), + }); + this._results.push({ + id: VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.PARTIALLY_VALID, + message, + }); + return; + } + } + + public createFailureToTokenize(message: string | undefined) { + this._results.push({ + id: VALIDATION_RESULT.FAILED_TO_TOKENIZE, + valid: CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID, + message: i18n.translate( + 'xpack.ml.models.jobService.categorization.messages.failureToGetTokens', + { + defaultMessage: + 'It was not possible to tokenize a sample of example field values. {message}', + values: { message: message || '' }, + } + ), + }); + } +} diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts index da23efa67d0b5c..da60a90f4bfbc1 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts @@ -5,4 +5,4 @@ */ export { newJobChartsProvider } from './charts'; -export { categorizationExamplesProvider } from './categorization'; +export { categorizationExamplesProvider, topCategoriesProvider } from './categorization'; diff --git a/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js b/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js index aaaf3d6ad40cf1..3df93bdb24f328 100644 --- a/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/__tests__/deprecations.js @@ -80,4 +80,54 @@ describe('monitoring plugin deprecations', function() { expect(log.called).to.be(true); }); }); + + describe('elasticsearch.username', function() { + it('logs a warning if elasticsearch.username is set to "elastic"', () => { + const settings = { elasticsearch: { username: 'elastic' } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + + it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => { + const settings = { elasticsearch: { username: 'otheruser' } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it('does not log a warning if elasticsearch.username is unset', () => { + const settings = { elasticsearch: { username: undefined } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it('logs a warning if ssl.key is set and ssl.certificate is not', () => { + const settings = { elasticsearch: { ssl: { key: '' } } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + + it('logs a warning if ssl.certificate is set and ssl.key is not', () => { + const settings = { elasticsearch: { ssl: { certificate: '' } } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + + it('does not log a warning if both ssl.key and ssl.certificate are set', () => { + const settings = { elasticsearch: { ssl: { key: '', certificate: '' } } }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + }); }); diff --git a/x-pack/legacy/plugins/monitoring/config.js b/x-pack/legacy/plugins/monitoring/config.js index c33b0b28e830ad..91c1ee99a0b2e4 100644 --- a/x-pack/legacy/plugins/monitoring/config.js +++ b/x-pack/legacy/plugins/monitoring/config.js @@ -83,6 +83,14 @@ export const config = Joi => { certificate: Joi.string(), key: Joi.string(), keyPassphrase: Joi.string(), + keystore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), + truststore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), alwaysPresentCertificate: Joi.boolean().default(false), }).default(), apiVersion: Joi.string().default('master'), diff --git a/x-pack/legacy/plugins/monitoring/deprecations.js b/x-pack/legacy/plugins/monitoring/deprecations.js index 13a6a58fa8752f..c3b2b70690f33b 100644 --- a/x-pack/legacy/plugins/monitoring/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/deprecations.js @@ -27,5 +27,31 @@ export const deprecations = () => { ); } }, + (settings, log) => { + const fromPath = 'xpack.monitoring.elasticsearch'; + const es = get(settings, 'elasticsearch'); + if (es) { + if (es.username === 'elastic') { + log( + `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana" user instead.` + ); + } + } + }, + (settings, log) => { + const fromPath = 'xpack.monitoring.elasticsearch.ssl'; + const ssl = get(settings, 'elasticsearch.ssl'); + if (ssl) { + if (ssl.key !== undefined && ssl.certificate === undefined) { + log( + `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` + ); + } else if (ssl.certificate !== undefined && ssl.key === undefined) { + log( + `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` + ); + } + } + }, ]; }; diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js b/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js index 8797f92e489bb1..6844bd5febf8ee 100644 --- a/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js +++ b/x-pack/legacy/plugins/monitoring/server/es_client/__tests__/instantiate_client.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { get, noop } from 'lodash'; +import { noop } from 'lodash'; import { exposeClient, hasMonitoringCluster } from '../instantiate_client'; function getMockServerFromConnectionUrl(monitoringClusterUrl) { @@ -26,15 +26,8 @@ function getMockServerFromConnectionUrl(monitoringClusterUrl) { }, }; - const config = { - get: path => { - return get(server, path); - }, - set: noop, - }; - return { - config, + elasticsearchConfig: server.xpack.monitoring.elasticsearch, elasticsearchPlugin: { getCluster: sinon .stub() @@ -141,12 +134,12 @@ describe('Instantiate Client', () => { describe('hasMonitoringCluster', () => { it('returns true if monitoring is configured', () => { const server = getMockServerFromConnectionUrl('http://monitoring-cluster.test:9200'); // pass null for URL to create the client using prod config - expect(hasMonitoringCluster(server.config)).to.be(true); + expect(hasMonitoringCluster(server.elasticsearchConfig)).to.be(true); }); it('returns false if monitoring is not configured', () => { const server = getMockServerFromConnectionUrl(null); - expect(hasMonitoringCluster(server.config)).to.be(false); + expect(hasMonitoringCluster(server.elasticsearchConfig)).to.be(false); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js b/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js index 87a2e5349cf1b7..9aed1ac1456174 100644 --- a/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js +++ b/x-pack/legacy/plugins/monitoring/server/es_client/instantiate_client.js @@ -14,24 +14,21 @@ import { LOGGING_TAG } from '../../common/constants'; * Kibana itself is connected to a production cluster. */ -export function exposeClient({ config, events, log, elasticsearchPlugin }) { - const elasticsearchConfig = hasMonitoringCluster(config) - ? config.get('xpack.monitoring.elasticsearch') - : {}; +export function exposeClient({ elasticsearchConfig, events, log, elasticsearchPlugin }) { + const isMonitoringCluster = hasMonitoringCluster(elasticsearchConfig); const cluster = elasticsearchPlugin.createCluster('monitoring', { - ...elasticsearchConfig, + ...(isMonitoringCluster ? elasticsearchConfig : {}), plugins: [monitoringBulk], logQueries: Boolean(elasticsearchConfig.logQueries), }); events.on('stop', bindKey(cluster, 'close')); - const configSource = hasMonitoringCluster(config) ? 'monitoring' : 'production'; + const configSource = isMonitoringCluster ? 'monitoring' : 'production'; log([LOGGING_TAG, 'es-client'], `config sourced from: ${configSource} cluster`); } export function hasMonitoringCluster(config) { - const hosts = config.get('xpack.monitoring.elasticsearch.hosts'); - return Boolean(hosts && hosts.length); + return Boolean(config.hosts && config.hosts.length); } export const instantiateClient = once(exposeClient); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts new file mode 100644 index 00000000000000..42141313ceea2a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.mocks.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockReadFileSync = jest.fn(); +jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); + +export const mockReadPkcs12Keystore = jest.fn(); +export const mockReadPkcs12Truststore = jest.fn(); +jest.mock('../../../../../../src/core/utils', () => ({ + readPkcs12Keystore: mockReadPkcs12Keystore, + readPkcs12Truststore: mockReadPkcs12Truststore, +})); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts new file mode 100644 index 00000000000000..c6f4e0fa685045 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + mockReadFileSync, + mockReadPkcs12Keystore, + mockReadPkcs12Truststore, +} from './parse_elasticsearch_config.test.mocks'; + +import { parseElasticsearchConfig } from './parse_elasticsearch_config'; + +const parse = (config: any) => { + return parseElasticsearchConfig({ + get: () => config, + }); +}; + +describe('reads files', () => { + beforeEach(() => { + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); + }); + + it('reads certificate authorities when ssl.keystore.path is specified', () => { + const configValue = parse({ ssl: { keystore: { path: 'some-path' } } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); + + it('reads certificate authorities when ssl.truststore.path is specified', () => { + const configValue = parse({ ssl: { truststore: { path: 'some-path' } } }); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + }); + + it('reads certificate authorities when ssl.certificateAuthorities is specified', () => { + let configValue = parse({ ssl: { certificateAuthorities: 'some-path' } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = parse({ ssl: { certificateAuthorities: ['some-path'] } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = parse({ ssl: { certificateAuthorities: ['some-path', 'another-path'] } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); + + it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => { + const configValue = parse({ + ssl: { + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }, + }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); + + it('reads a private key and certificate when ssl.keystore.path is specified', () => { + const configValue = parse({ ssl: { keystore: { path: 'some-path' } } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path.key'); + expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert'); + }); + + it('reads a private key when ssl.key is specified', () => { + const configValue = parse({ ssl: { key: 'some-path' } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path'); + }); + + it('reads a certificate when ssl.certificate is specified', () => { + const configValue = parse({ ssl: { certificate: 'some-path' } }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificate).toEqual('content-of-some-path'); + }); +}); + +describe('throws when config is invalid', () => { + beforeAll(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../../../../../src/core/utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); + }); + + it('throws if key is invalid', () => { + const value = { ssl: { key: '/invalid/key' } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); + + it('throws if certificate is invalid', () => { + const value = { ssl: { certificate: '/invalid/cert' } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/cert'"` + ); + }); + + it('throws if certificateAuthorities is invalid', () => { + const value = { ssl: { certificateAuthorities: '/invalid/ca' } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/ca'"` + ); + }); + + it('throws if keystore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/keystore' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); + + it('throws if keystore does not contain a key', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({}); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"Did not find key in Elasticsearch keystore."` + ); + }); + + it('throws if keystore does not contain a certificate', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({ key: 'foo' }); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"Did not find certificate in Elasticsearch keystore."` + ); + }); + + it('throws if truststore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/truststore' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); + + it('throws if key and keystore.path are both specified', () => { + const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"[config validation of [xpack.monitoring.elasticsearch].ssl]: cannot use [key] when [keystore.path] is specified"` + ); + }); + + it('throws if certificate and keystore.path are both specified', () => { + const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } }; + expect(() => parse(value)).toThrowErrorMatchingInlineSnapshot( + `"[config validation of [xpack.monitoring.elasticsearch].ssl]: cannot use [certificate] when [keystore.path] is specified"` + ); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts new file mode 100644 index 00000000000000..70e6235602b5b9 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/es_client/parse_elasticsearch_config.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { readFileSync } from 'fs'; +import { readPkcs12Truststore, readPkcs12Keystore } from '../../../../../../src/core/utils'; + +const KEY = 'xpack.monitoring.elasticsearch'; + +/* + * Parse a config object's Elasticsearch configuration, reading any + * certificates/keys from the filesystem + * + * TODO: this code can be removed when this plugin is migrated to the Kibana Platform, + * at that point the ElasticsearchClient and ElasticsearchConfig should be used instead + */ +export const parseElasticsearchConfig = (config: any) => { + const es = config.get(KEY); + + const errorPrefix = `[config validation of [${KEY}].ssl]`; + if (es.ssl?.key && es.ssl?.keystore?.path) { + throw new Error(`${errorPrefix}: cannot use [key] when [keystore.path] is specified`); + } + if (es.ssl?.certificate && es.ssl?.keystore?.path) { + throw new Error(`${errorPrefix}: cannot use [certificate] when [keystore.path] is specified`); + } + + const { alwaysPresentCertificate, verificationMode } = es.ssl; + const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(es); + + return { + ...es, + ssl: { + alwaysPresentCertificate, + key, + keyPassphrase, + certificate, + certificateAuthorities, + verificationMode, + }, + }; +}; + +const readKeyAndCerts = (rawConfig: any) => { + let key: string | undefined; + let keyPassphrase: string | undefined; + let certificate: string | undefined; + let certificateAuthorities: string[] | undefined; + + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + certificateAuthorities = [...(certificateAuthorities || []), ...ca]; + } + }; + + if (rawConfig.ssl.keystore?.path) { + const keystore = readPkcs12Keystore( + rawConfig.ssl.keystore.path, + rawConfig.ssl.keystore.password + ); + if (!keystore.key) { + throw new Error(`Did not find key in Elasticsearch keystore.`); + } else if (!keystore.cert) { + throw new Error(`Did not find certificate in Elasticsearch keystore.`); + } + key = keystore.key; + certificate = keystore.cert; + addCAs(keystore.ca); + } else { + if (rawConfig.ssl.key) { + key = readFile(rawConfig.ssl.key); + keyPassphrase = rawConfig.ssl.keyPassphrase; + } + if (rawConfig.ssl.certificate) { + certificate = readFile(rawConfig.ssl.certificate); + } + } + + if (rawConfig.ssl.truststore?.path) { + const ca = readPkcs12Truststore( + rawConfig.ssl.truststore.path, + rawConfig.ssl.truststore.password + ); + addCAs(ca); + } + + const ca = rawConfig.ssl.certificateAuthorities; + if (ca) { + const parsed: string[] = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + addCAs(parsed); + } + } + + return { + key, + keyPassphrase, + certificate, + certificateAuthorities, + }; +}; + +const readFile = (file: string) => { + return readFileSync(file, 'utf8'); +}; diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index e26dd96dde1bf7..163bc43945be1c 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -11,6 +11,7 @@ import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; +import { parseElasticsearchConfig } from './es_client/parse_elasticsearch_config'; export class Plugin { setup(core, plugins) { @@ -36,6 +37,12 @@ export class Plugin { * fetch methods and uploads to the ES monitoring bulk endpoint */ const xpackMainPlugin = plugins.xpack_main; + + /* + * Parse the Elasticsearch config and read any certificates/keys if necessary + */ + const elasticsearchConfig = parseElasticsearchConfig(config); + xpackMainPlugin.status.once('green', async () => { // first time xpack_main turns green /* @@ -47,7 +54,7 @@ export class Plugin { await instantiateClient({ log: core.log, events: core.events, - config, + elasticsearchConfig, elasticsearchPlugin: plugins.elasticsearch, }); // Instantiate the dedicated ES client await initMonitoringXpackInfo({ diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index ba659aa74f10ca..2b5ea21a2bb452 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; /** * Configuration of dependency objects for the UI, which are needed for the @@ -26,6 +27,7 @@ export const getUiExports = () => ({ euiIconType: 'monitoringApp', linkToLastSubUrl: false, main: 'plugins/monitoring/monitoring', + category: DEFAULT_APP_CATEGORIES.management, }, injectDefaultVars(server) { const config = server.config(); diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css index ab88e4780936ea..2c203e507260fa 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css @@ -53,9 +53,10 @@ discover-app .discover-table-footer { * Visualize Editor Tweaks */ -/* hide unusable controls */ -visualization-editor .visEditor--default > :not(.visEditor__canvas) { - display: none; +/* hide unusable controls +* !important is required to override resizable panel inline display */ +visualization-editor .visEditor--default > :not(.visEditor__visualization) { + display: none !important; } /** THIS IS FOR TSVB UNTIL REFACTOR **/ diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css index 8aca042144b3b4..b5c9861208b7bd 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css @@ -52,9 +52,10 @@ discover-app .discover-table-footer { * Visualize Editor Tweaks */ -/* hide unusable controls */ -visualization-editor .visEditor--default > :not(.visEditor__canvas) { - display: none; +/* hide unusable controls +* !important is required to override resizable panel inline display */ +visualization-editor .visEditor--default > :not(.visEditor__visualization) { + display: none !important; } /** THIS IS FOR TSVB UNTIL REFACTOR **/ .tvbEditorVisualization { diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index fef45f5a5eae85..ae603d93245a32 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -86,7 +86,6 @@ export const executeJobFactory: ExecuteJobFactory { - // @ts-ignore fieldFormatServiceFactory' does not exist on type 'ServerFacade TODO const fieldFormats = await server.fieldFormatServiceFactory(uiConfig); return fieldFormatMapFactory(indexPatternSavedObject, fieldFormats); })(), diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 7e12adefca38d3..d39d2bbf08c9f8 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -57,7 +57,7 @@ export async function generateCsvSearch( jobParams: JobParamsDiscoverCsv ): Promise { const { savedObjects, uiSettingsServiceFactory } = server; - const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(req); + const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(req.getRawRequest()); const { indexPatternSavedObjectId, timerange } = searchPanel; const savedSearchObjectAttr = searchPanel.attributes as SavedSearchObjectAttributes; const { indexPatternSavedObject } = await getDataSource( diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index ef0ab37738362d..52e26b3132007d 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -6,21 +6,16 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; +import { Legacy } from 'kibana'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; -// @ts-ignore untyped module defintition -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { registerRoutes } from './server/routes'; -import { - LevelLogger, - checkLicenseFactory, - getExportTypesRegistry, - runValidations, -} from './server/lib'; +import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types.d'; import { config as reportingConfig } from './config'; -import { logConfiguration } from './log_configuration'; -import { createBrowserDriverFactory } from './server/browsers'; -import { registerReportingUsageCollector } from './server/usage'; -import { ReportingConfigOptions, ReportingPluginSpecOptions, ServerFacade } from './types.d'; +import { + LegacySetup, + ReportingPlugin, + ReportingSetupDeps, + reportingPluginFactory, +} from './server/plugin'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); @@ -42,7 +37,7 @@ export const reporting = (kibana: any) => { embeddableActions: ['plugins/reporting/panel_actions/get_csv_panel_action'], home: ['plugins/reporting/register_feature'], managementSections: ['plugins/reporting/views/management'], - injectDefaultVars(server: ServerFacade, options?: ReportingConfigOptions) { + injectDefaultVars(server: Legacy.Server, options?: ReportingConfigOptions) { const config = server.config(); return { reportingPollConfig: options ? options.poll : {}, @@ -70,41 +65,29 @@ export const reporting = (kibana: any) => { }, }, - // TODO: Decouple Hapi: Build a server facade object based on the server to - // pass through to the libs. Do not pass server directly - async init(server: ServerFacade) { - const exportTypesRegistry = getExportTypesRegistry(); - - let isCollectorReady = false; - // Register a function with server to manage the collection of usage stats - const { usageCollection } = server.newPlatform.setup.plugins; - registerReportingUsageCollector( - usageCollection, - server, - () => isCollectorReady, - exportTypesRegistry - ); - - const logger = LevelLogger.createForServer(server, [PLUGIN_ID]); - const browserDriverFactory = await createBrowserDriverFactory(server); - - logConfiguration(server, logger); - runValidations(server, logger, browserDriverFactory); - - const { xpack_main: xpackMainPlugin } = server.plugins; - mirrorPluginStatus(xpackMainPlugin, this); - const checkLicense = checkLicenseFactory(exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(this.id).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // Post initialization of the above code, the collector is now ready to fetch its data - isCollectorReady = true; + async init(server: Legacy.Server) { + const coreSetup = server.newPlatform.setup.core; + const pluginsSetup: ReportingSetupDeps = { + usageCollection: server.newPlatform.setup.plugins.usageCollection, + }; + const __LEGACY: LegacySetup = { + config: server.config, + info: server.info, + route: server.route.bind(server), + plugins: { + elasticsearch: server.plugins.elasticsearch, + xpack_main: server.plugins.xpack_main, + security: server.plugins.security, + }, + savedObjects: server.savedObjects, + uiSettingsServiceFactory: server.uiSettingsServiceFactory, + // @ts-ignore Property 'fieldFormatServiceFactory' does not exist on type 'Server'. + fieldFormatServiceFactory: server.fieldFormatServiceFactory, + log: server.log.bind(server), + }; - // Reporting routes - registerRoutes(server, exportTypesRegistry, browserDriverFactory, logger); + const plugin: ReportingPlugin = reportingPluginFactory(__LEGACY, this); + await plugin.setup(coreSetup, pluginsSetup); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index c1777038cc7d46..de8449ff29132f 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trunc } from 'lodash'; +import { trunc, map } from 'lodash'; import open from 'opn'; import { parse as parseUrl } from 'url'; import { Page, SerializableOrJSHandle, EvaluateFn } from 'puppeteer'; @@ -15,6 +15,7 @@ import { ConditionalHeaders, ConditionalHeadersConditions, ElementPosition, + InterceptedRequest, NetworkPolicy, } from '../../../../types'; @@ -59,35 +60,57 @@ export class HeadlessChromiumDriver { }: { conditionalHeaders: ConditionalHeaders; waitForSelector: string }, logger: LevelLogger ) { - await this.page.setRequestInterception(true); logger.info(`opening url ${url}`); + // @ts-ignore + const client = this.page._client; let interceptedCount = 0; - this.page.on('request', interceptedRequest => { - const interceptedUrl = interceptedRequest.url(); + await this.page.setRequestInterception(true); + + // We have to reach into the Chrome Devtools Protocol to apply headers as using + // puppeteer's API will cause map tile requests to hang indefinitely: + // https://github.com/puppeteer/puppeteer/issues/5003 + // Docs on this client/protocol can be found here: + // https://chromedevtools.github.io/devtools-protocol/tot/Fetch + client.on('Fetch.requestPaused', (interceptedRequest: InterceptedRequest) => { + const { + requestId, + request: { url: interceptedUrl }, + } = interceptedRequest; const allowed = !interceptedUrl.startsWith('file://'); const isData = interceptedUrl.startsWith('data:'); // We should never ever let file protocol requests go through if (!allowed || !this.allowRequest(interceptedUrl)) { logger.error(`Got bad URL: "${interceptedUrl}", closing browser.`); - interceptedRequest.abort('blockedbyclient'); + client.send('Fetch.failRequest', { + errorReason: 'Aborted', + requestId, + }); this.page.browser().close(); throw new Error(`Received disallowed outgoing URL: "${interceptedUrl}", exiting`); } if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedUrl)) { logger.debug(`Using custom headers for ${interceptedUrl}`); - interceptedRequest.continue({ - headers: { - ...interceptedRequest.headers(), + const headers = map( + { + ...interceptedRequest.request.headers, ...conditionalHeaders.headers, }, + (value, name) => ({ + name, + value, + }) + ); + client.send('Fetch.continueRequest', { + requestId, + headers, }); } else { const loggedUrl = isData ? this.truncateUrl(interceptedUrl) : interceptedUrl; logger.debug(`No custom headers for ${loggedUrl}`); - interceptedRequest.continue(); + client.send('Fetch.continueRequest', { requestId }); } interceptedCount = interceptedCount + (isData ? 0 : 1); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/get_user.js b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts similarity index 57% rename from x-pack/legacy/plugins/reporting/server/lib/get_user.js rename to x-pack/legacy/plugins/reporting/server/lib/get_user.ts index 04c9516cb99d4f..e2921de795012b 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/get_user.js +++ b/x-pack/legacy/plugins/reporting/server/lib/get_user.ts @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getUserFactory(server) { - return async request => { +import { Legacy } from 'kibana'; +import { ServerFacade } from '../../types'; + +export function getUserFactory(server: ServerFacade) { + /* + * Legacy.Request because this is called from routing middleware + */ + return async (request: Legacy.Request) => { if (!server.plugins.security) { return null; } @@ -13,7 +19,7 @@ export function getUserFactory(server) { try { return await server.plugins.security.getUser(request); } catch (err) { - server.log(['reporting', 'getUser', 'debug'], err); + server.log(['reporting', 'getUser', 'error'], err); return null; } }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts b/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts index c67a9cd32d50d2..839fa16a716b7d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -type ServerLog = (tags: string[], msg: string) => void; +import { ServerFacade } from '../../types'; const trimStr = (toTrim: string) => { return typeof toTrim === 'string' ? toTrim.trim() : toTrim; @@ -16,12 +16,12 @@ export class LevelLogger { public warn: (msg: string, tags?: string[]) => void; - static createForServer(server: any, tags: string[]) { - const serverLog: ServerLog = (tgs: string[], msg: string) => server.log(tgs, msg); + static createForServer(server: ServerFacade, tags: string[]) { + const serverLog: ServerFacade['log'] = (tgs: string[], msg: string) => server.log(tgs, msg); return new LevelLogger(serverLog, tags); } - constructor(logger: ServerLog, tags: string[]) { + constructor(logger: ServerFacade['log'], tags: string[]) { this._logger = logger; this._tags = tags; diff --git a/x-pack/legacy/plugins/reporting/server/lib/once_per_server.ts b/x-pack/legacy/plugins/reporting/server/lib/once_per_server.ts index d73a5b73fecd0a..ae3636079a9bb6 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/once_per_server.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/once_per_server.ts @@ -27,10 +27,6 @@ export function oncePerServer(fn: ServerFn) { throw new TypeError('This function expects to be called with a single argument'); } - if (!server || typeof server.expose !== 'function') { - throw new TypeError('This function expects to be passed the server'); - } - // @ts-ignore return fn.call(this, server); }); diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts new file mode 100644 index 00000000000000..934a3487209c42 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Legacy } from 'kibana'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/server'; +import { IUiSettingsClient } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; +// @ts-ignore +import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; +import { PLUGIN_ID } from '../common/constants'; +import { ReportingPluginSpecOptions } from '../types.d'; +import { registerRoutes } from './routes'; +import { LevelLogger, checkLicenseFactory, getExportTypesRegistry, runValidations } from './lib'; +import { createBrowserDriverFactory } from './browsers'; +import { registerReportingUsageCollector } from './usage'; +import { logConfiguration } from '../log_configuration'; + +// For now there is no exposed functionality to other plugins +export type ReportingSetup = object; +export type ReportingStart = object; + +export interface ReportingSetupDeps { + usageCollection: UsageCollectionSetup; +} +export type ReportingStartDeps = object; + +type LegacyPlugins = Legacy.Server['plugins']; + +export interface LegacySetup { + config: Legacy.Server['config']; + info: Legacy.Server['info']; + log: Legacy.Server['log']; + plugins: { + elasticsearch: LegacyPlugins['elasticsearch']; + security: LegacyPlugins['security']; + xpack_main: XPackMainPlugin & { + status?: any; + }; + }; + route: Legacy.Server['route']; + savedObjects: Legacy.Server['savedObjects']; + uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; + fieldFormatServiceFactory: (uiConfig: IUiSettingsClient) => unknown; +} + +export type ReportingPlugin = Plugin< + ReportingSetup, + ReportingStart, + ReportingSetupDeps, + ReportingStartDeps +>; + +/* We need a factory that returns an instance of the class because the class + * implementation itself restricts against having Legacy dependencies passed + * into `setup`. The factory parameters take the legacy dependencies, and the + * `setup` method gets it from enclosure */ +export function reportingPluginFactory( + __LEGACY: LegacySetup, + legacyPlugin: ReportingPluginSpecOptions +) { + return new (class ReportingPlugin implements ReportingPlugin { + public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise { + const exportTypesRegistry = getExportTypesRegistry(); + + let isCollectorReady = false; + // Register a function with server to manage the collection of usage stats + const { usageCollection } = plugins; + registerReportingUsageCollector( + usageCollection, + __LEGACY, + () => isCollectorReady, + exportTypesRegistry + ); + + const logger = LevelLogger.createForServer(__LEGACY, [PLUGIN_ID]); + const browserDriverFactory = await createBrowserDriverFactory(__LEGACY); + + logConfiguration(__LEGACY, logger); + runValidations(__LEGACY, logger, browserDriverFactory); + + const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; + mirrorPluginStatus(xpackMainPlugin, legacyPlugin); + const checkLicense = checkLicenseFactory(exportTypesRegistry); + (xpackMainPlugin as any).status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); + }); + + // Post initialization of the above code, the collector is now ready to fetch its data + isCollectorReady = true; + + // Reporting routes + registerRoutes(__LEGACY, exportTypesRegistry, browserDriverFactory, logger); + + return {}; + } + + public start(core: CoreStart, plugins: ReportingStartDeps): ReportingStart { + return {}; + } + })(); +} diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 128cc44db4dc48..c9225dfee69788 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Legacy } from 'kibana'; import boom from 'boom'; import Joi from 'joi'; import rison from 'rison-node'; import { API_BASE_URL } from '../../common/constants'; -import { ServerFacade, RequestFacade, ReportingResponseToolkit } from '../../types'; +import { ServerFacade, ReportingResponseToolkit } from '../../types'; import { getRouteConfigFactoryReportingPre, GetRouteConfigFactoryFn, RouteConfigFactory, } from './lib/route_config_factories'; +import { makeRequestFacade } from './lib/make_request_facade'; import { HandlerErrorFunction, HandlerFunction } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; @@ -54,7 +56,8 @@ export function registerGenerateFromJobParams( path: `${BASE_GENERATE}/{exportType}`, method: 'POST', options: getRouteConfig(), - handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { + handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { + const request = makeRequestFacade(legacyRequest); let jobParamsRison: string | null; if (request.payload) { @@ -80,7 +83,7 @@ export function registerGenerateFromJobParams( if (!jobParams) { throw new Error('missing jobParams!'); } - response = await handler(exportType, jobParams, request, h); + response = await handler(exportType, jobParams, legacyRequest, h); } catch (err) { throw boom.badRequest(`invalid rison: ${jobParamsRison}`); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts index 72c8055614065f..2c509136b1b441 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Legacy } from 'kibana'; import { get } from 'lodash'; import { API_BASE_GENERATE_V1, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../common/constants'; -import { ServerFacade, RequestFacade, ReportingResponseToolkit } from '../../types'; +import { ServerFacade, ReportingResponseToolkit } from '../../types'; import { HandlerErrorFunction, HandlerFunction, QueuedJobPayload } from './types'; import { getRouteOptionsCsv } from './lib/route_config_factories'; +import { makeRequestFacade } from './lib/make_request_facade'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; /* @@ -31,17 +33,18 @@ export function registerGenerateCsvFromSavedObject( path: `${API_BASE_GENERATE_V1}/csv/saved-object/{savedObjectType}:{savedObjectId}`, method: 'POST', options: routeOptions, - handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { + handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { + const requestFacade = makeRequestFacade(legacyRequest); + /* * 1. Build `jobParams` object: job data that execution will need to reference in various parts of the lifecycle * 2. Pass the jobParams and other common params to `handleRoute`, a shared function to enqueue the job with the params * 3. Ensure that details for a queued job were returned */ - let result: QueuedJobPayload; try { - const jobParams = getJobParamsFromRequest(request, { isImmediate: false }); - result = await handleRoute(CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobParams, request, h); + const jobParams = getJobParamsFromRequest(requestFacade, { isImmediate: false }); + result = await handleRoute(CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobParams, legacyRequest, h); // pass the original request because the handler will make the request facade on its own } catch (err) { throw handleRouteError(CSV_FROM_SAVEDOBJECT_JOB_TYPE, err); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index bc96c27f64c103..8d1c84664cbe9d 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Legacy } from 'kibana'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFactory, executeJobFactory } from '../../export_types/csv_from_savedobject'; import { ServerFacade, - RequestFacade, ResponseFacade, HeadlessChromiumDriverFactory, ReportingResponseToolkit, @@ -16,8 +16,9 @@ import { JobDocOutputExecuted, } from '../../types'; import { JobDocPayloadPanelCsv } from '../../export_types/csv_from_savedobject/types'; -import { getRouteOptionsCsv } from './lib/route_config_factories'; import { getJobParamsFromRequest } from '../../export_types/csv_from_savedobject/server/lib/get_job_params_from_request'; +import { getRouteOptionsCsv } from './lib/route_config_factories'; +import { makeRequestFacade } from './lib/make_request_facade'; /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: @@ -43,7 +44,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, method: 'POST', options: routeOptions, - handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { + handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { + const request = makeRequestFacade(legacyRequest); const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 73450b7641c8e4..21af54ddf11e36 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -5,12 +5,12 @@ */ import boom from 'boom'; +import { Legacy } from 'kibana'; import { API_BASE_URL } from '../../common/constants'; import { ServerFacade, ExportTypesRegistry, HeadlessChromiumDriverFactory, - RequestFacade, ReportingResponseToolkit, Logger, } from '../../types'; @@ -18,6 +18,7 @@ import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; import { createQueueFactory, enqueueJobFactory } from '../lib'; +import { makeRequestFacade } from './lib/make_request_facade'; export function registerJobGenerationRoutes( server: ServerFacade, @@ -39,9 +40,10 @@ export function registerJobGenerationRoutes( async function handler( exportTypeId: string, jobParams: object, - request: RequestFacade, + legacyRequest: Legacy.Request, h: ReportingResponseToolkit ) { + const request = makeRequestFacade(legacyRequest); const user = request.pre.user; const headers = request.headers; diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index fd5014911d262a..a0be15d60f3164 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Legacy } from 'kibana'; import boom from 'boom'; import { API_BASE_URL } from '../../common/constants'; import { ServerFacade, ExportTypesRegistry, Logger, - RequestFacade, ReportingResponseToolkit, JobDocOutput, JobSource, + ListQuery, } from '../../types'; // @ts-ignore import { jobsQueryFactory } from '../lib/jobs_query'; @@ -23,6 +24,7 @@ import { getRouteConfigFactoryDownloadPre, getRouteConfigFactoryManagementPre, } from './lib/route_config_factories'; +import { makeRequestFacade } from './lib/make_request_facade'; const MAIN_ENTRY = `${API_BASE_URL}/jobs`; @@ -40,8 +42,9 @@ export function registerJobInfoRoutes( path: `${MAIN_ENTRY}/list`, method: 'GET', options: getRouteConfig(), - handler: (request: RequestFacade) => { - const { page: queryPage, size: querySize, ids: queryIds } = request.query; + handler: (legacyRequest: Legacy.Request) => { + const request = makeRequestFacade(legacyRequest); + const { page: queryPage, size: querySize, ids: queryIds } = request.query as ListQuery; const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; @@ -62,7 +65,8 @@ export function registerJobInfoRoutes( path: `${MAIN_ENTRY}/count`, method: 'GET', options: getRouteConfig(), - handler: (request: RequestFacade) => { + handler: (legacyRequest: Legacy.Request) => { + const request = makeRequestFacade(legacyRequest); const results = jobsQuery.count(request.pre.management.jobTypes, request.pre.user); return results; }, @@ -73,7 +77,8 @@ export function registerJobInfoRoutes( path: `${MAIN_ENTRY}/output/{docId}`, method: 'GET', options: getRouteConfig(), - handler: (request: RequestFacade) => { + handler: (legacyRequest: Legacy.Request) => { + const request = makeRequestFacade(legacyRequest); const { docId } = request.params; return jobsQuery.get(request.pre.user, docId, { includeContent: true }).then( @@ -98,7 +103,8 @@ export function registerJobInfoRoutes( path: `${MAIN_ENTRY}/info/{docId}`, method: 'GET', options: getRouteConfig(), - handler: (request: RequestFacade) => { + handler: (legacyRequest: Legacy.Request) => { + const request = makeRequestFacade(legacyRequest); const { docId } = request.params; return jobsQuery @@ -130,7 +136,8 @@ export function registerJobInfoRoutes( path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', options: getRouteConfigDownload(), - handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { + handler: async (legacyRequest: Legacy.Request, h: ReportingResponseToolkit) => { + const request = makeRequestFacade(legacyRequest); const { docId } = request.params; let response = await jobResponseHandler( diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.test.ts new file mode 100644 index 00000000000000..8cdb7b4c018d7b --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.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 { Legacy } from 'kibana'; +import { makeRequestFacade } from './make_request_facade'; + +describe('makeRequestFacade', () => { + test('creates a default object', () => { + const legacyRequest = ({ + getBasePath: () => 'basebase', + params: { + param1: 123, + }, + payload: { + payload1: 123, + }, + headers: { + user: 123, + }, + } as unknown) as Legacy.Request; + + expect(makeRequestFacade(legacyRequest)).toMatchInlineSnapshot(` + Object { + "getBasePath": [Function], + "getRawRequest": [Function], + "getSavedObjectsClient": undefined, + "headers": Object { + "user": 123, + }, + "params": Object { + "param1": 123, + }, + "payload": Object { + "payload1": 123, + }, + "pre": undefined, + "query": undefined, + "route": undefined, + } + `); + }); + + test('getRawRequest', () => { + const legacyRequest = ({ + getBasePath: () => 'basebase', + params: { + param1: 123, + }, + payload: { + payload1: 123, + }, + headers: { + user: 123, + }, + } as unknown) as Legacy.Request; + + expect(makeRequestFacade(legacyRequest).getRawRequest()).toBe(legacyRequest); + }); +}); diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts new file mode 100644 index 00000000000000..fb8a2dbbff17b1 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/make_request_facade.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { RequestQuery } from 'hapi'; +import { Legacy } from 'kibana'; +import { + RequestFacade, + ReportingRequestPayload, + ReportingRequestPre, + ReportingRequestQuery, +} from '../../../types'; + +export function makeRequestFacade(request: Legacy.Request): RequestFacade { + // This condition is for unit tests + const getSavedObjectsClient = request.getSavedObjectsClient + ? request.getSavedObjectsClient.bind(request) + : request.getSavedObjectsClient; + return { + getSavedObjectsClient, + headers: request.headers, + params: request.params, + payload: (request.payload as object) as ReportingRequestPayload, + query: ((request.query as RequestQuery) as object) as ReportingRequestQuery, + pre: (request.pre as Record) as ReportingRequestPre, + getBasePath: request.getBasePath, + route: request.route, + getRawRequest: () => request, + }; +} diff --git a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts index b50d443ec00b9d..f3660a22cbac1e 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Legacy } from 'kibana'; import { RequestFacade, ReportingResponseToolkit, JobDocPayload } from '../../types'; export type HandlerFunction = ( exportType: string, jobParams: object, - request: RequestFacade, + request: Legacy.Request, h: ReportingResponseToolkit ) => any; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index c17b969d5d7fac..9fae60afee4e82 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -7,7 +7,6 @@ import { ResponseObject } from 'hapi'; import { EventEmitter } from 'events'; import { Legacy } from 'kibana'; -import { XPackMainPlugin } from '../xpack_main/server/xpack_main'; import { ElasticsearchPlugin, CallCluster, @@ -16,6 +15,7 @@ import { CancellationToken } from './common/cancellation_token'; import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; +import { LegacySetup } from './server/plugin'; export type ReportingPlugin = object; // For Plugin contract @@ -53,7 +53,7 @@ export interface NetworkPolicy { rules: NetworkPolicyRule[]; } -interface ListQuery { +export interface ListQuery { page: string; size: string; ids?: string; // optional field forbids us from extending RequestQuery @@ -64,35 +64,14 @@ interface GenerateQuery { interface GenerateExportTypePayload { jobParams: string; } -interface DownloadParams { - docId: string; -} /* * Legacy System */ -export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; - -export type ServerFacade = Legacy.Server & { - plugins: { - xpack_main?: XPackMainPlugin & { - status?: any; - }; - }; -}; +export type ServerFacade = LegacySetup; -interface ReportingRequest { - query: ListQuery & GenerateQuery; - params: DownloadParams; - payload: GenerateExportTypePayload; - pre: { - management: { - jobTypes: any; - }; - user: any; - }; -} +export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; export type EnqueueJobFn = ( parentLogger: LevelLogger, @@ -103,7 +82,27 @@ export type EnqueueJobFn = ( request: RequestFacade ) => Promise; -export type RequestFacade = ReportingRequest & Legacy.Request; +export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPayload; +export type ReportingRequestQuery = ListQuery | GenerateQuery; + +export interface ReportingRequestPre { + management: { + jobTypes: any; + }; + user: any; // TODO import AuthenticatedUser from security/server +} + +export interface RequestFacade { + getBasePath: Legacy.Request['getBasePath']; + getSavedObjectsClient: Legacy.Request['getSavedObjectsClient']; + headers: Legacy.Request['headers']; + params: Legacy.Request['params']; + payload: JobParamPostPayload | GenerateExportTypePayload; + query: ReportingRequestQuery; + route: Legacy.Request['route']; + pre: ReportingRequestPre; + getRawRequest: () => Legacy.Request; +} export type ResponseFacade = ResponseObject & { isBoom: boolean; @@ -341,3 +340,18 @@ export interface AbsoluteURLFactoryOptions { hostname: string; port: string | number; } + +export interface InterceptedRequest { + requestId: string; + request: { + url: string; + method: string; + headers: { + [key: string]: string; + }; + initialPriority: string; + referrerPolicy: string; + }; + frameId: string; + resourceType: string; +} diff --git a/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js b/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js index 590f3dc85740ed..897caa07fd8739 100644 --- a/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js +++ b/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js @@ -9,7 +9,7 @@ import { editorConfigProviders } from 'ui/vis/editors/config/editor_config_provi export function initEditorConfig() { // Limit agg params based on rollup capabilities - editorConfigProviders.register((aggType, indexPattern, aggConfig) => { + editorConfigProviders.register((indexPattern, aggConfig) => { if (indexPattern.type !== 'rollup') { return {}; } diff --git a/x-pack/legacy/plugins/security/common/model.ts b/x-pack/legacy/plugins/security/common/model.ts deleted file mode 100644 index 733e89f774db8c..00000000000000 --- a/x-pack/legacy/plugins/security/common/model.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { - ApiKey, - ApiKeyToInvalidate, - AuthenticatedUser, - BuiltinESPrivileges, - EditUser, - FeaturesPrivileges, - InlineRoleTemplate, - InvalidRoleTemplate, - KibanaPrivileges, - RawKibanaFeaturePrivileges, - RawKibanaPrivileges, - Role, - RoleIndexPrivilege, - RoleKibanaPrivilege, - RoleMapping, - RoleTemplate, - StoredRoleTemplate, - User, - canUserChangePassword, - getUserDisplayName, -} from '../../../../plugins/security/common/model'; diff --git a/x-pack/legacy/plugins/security/index.d.ts b/x-pack/legacy/plugins/security/index.d.ts index 18284c8be689a1..d453415f73376c 100644 --- a/x-pack/legacy/plugins/security/index.d.ts +++ b/x-pack/legacy/plugins/security/index.d.ts @@ -5,7 +5,7 @@ */ import { Legacy } from 'kibana'; -import { AuthenticatedUser } from './common/model'; +import { AuthenticatedUser } from '../../../plugins/security/public'; /** * Public interface of the security plugin. diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index bc403b803b8df0..4988c30a1398b0 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -40,8 +40,6 @@ export const security = kibana => }, uiExports: { - chromeNavControls: [], - managementSections: ['plugins/security/views/management'], styleSheetPaths: resolve(__dirname, 'public/index.scss'), apps: [ { @@ -76,7 +74,6 @@ export const security = kibana => 'plugins/security/hacks/on_unauthorized_response', 'plugins/security/hacks/register_account_management_app', ], - home: ['plugins/security/register_feature'], injectDefaultVars: server => { const securityPlugin = server.newPlatform.setup.plugins.security; if (!securityPlugin) { diff --git a/x-pack/legacy/plugins/security/public/documentation_links.js b/x-pack/legacy/plugins/security/public/documentation_links.js deleted file mode 100644 index 8050289b95e9df..00000000000000 --- a/x-pack/legacy/plugins/security/public/documentation_links.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; - -const ES_REF_URL = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; - -export const documentationLinks = { - dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html`, - esClusterPrivileges: `${ES_REF_URL}/security-privileges.html#privileges-list-cluster`, - esIndicesPrivileges: `${ES_REF_URL}/security-privileges.html#privileges-list-indices`, - esRunAsPrivileges: `${ES_REF_URL}/security-privileges.html#_run_as_privilege`, -}; diff --git a/x-pack/legacy/plugins/security/public/images/logout.svg b/x-pack/legacy/plugins/security/public/images/logout.svg deleted file mode 100644 index d6533c07199040..00000000000000 --- a/x-pack/legacy/plugins/security/public/images/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/images/person.svg b/x-pack/legacy/plugins/security/public/images/person.svg deleted file mode 100644 index 988ddac8859d71..00000000000000 --- a/x-pack/legacy/plugins/security/public/images/person.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/index.scss b/x-pack/legacy/plugins/security/public/index.scss index 2d7696bed39890..187ad5231534d2 100644 --- a/x-pack/legacy/plugins/security/public/index.scss +++ b/x-pack/legacy/plugins/security/public/index.scss @@ -15,3 +15,6 @@ $secFormWidth: 460px; // Public views @import './views/index'; +// Styles of Kibana Platform plugin +@import '../../../../plugins/security/public/index'; + diff --git a/x-pack/legacy/plugins/security/public/lib/__tests__/util.js b/x-pack/legacy/plugins/security/public/lib/__tests__/util.js deleted file mode 100644 index 3f7d8aea53a85f..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/__tests__/util.js +++ /dev/null @@ -1,49 +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 { toggle, toggleSort } from '../../../public/lib/util'; - -describe('util', () => { - describe('toggle', () => { - it('should add an item to a collection if not already included', () => { - const collection = [1, 2, 3, 4, 5]; - toggle(collection, 6); - expect(collection.indexOf(6)).to.be.above(0); - }); - - it('should remove an item from a collection if already included', () => { - const collection = [1, 2, 3, 4, 5]; - toggle(collection, 3); - expect(collection.indexOf(3)).to.be.below(0); - }); - }); - - describe('toggleSort', () => { - it('should toggle reverse if called with the same orderBy', () => { - const sort = { orderBy: 'foo', reverse: false }; - - toggleSort(sort, 'foo'); - expect(sort.reverse).to.be.true; - - toggleSort(sort, 'foo'); - expect(sort.reverse).to.be.false; - }); - - it('should change orderBy and set reverse to false when called with a different orderBy', () => { - const sort = { orderBy: 'foo', reverse: false }; - - toggleSort(sort, 'bar'); - expect(sort.orderBy).to.equal('bar'); - expect(sort.reverse).to.be.false; - - sort.reverse = true; - toggleSort(sort, 'foo'); - expect(sort.orderBy).to.equal('foo'); - expect(sort.reverse).to.be.false; - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/lib/api.ts b/x-pack/legacy/plugins/security/public/lib/api.ts deleted file mode 100644 index c5c6994bf4be36..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/api.ts +++ /dev/null @@ -1,56 +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 { kfetch } from 'ui/kfetch'; -import { Role, User, EditUser } from '../../common/model'; - -const usersUrl = '/internal/security/users'; -const rolesUrl = '/api/security/role'; - -export class UserAPIClient { - public async getUsers(): Promise { - return await kfetch({ pathname: usersUrl }); - } - - public async getUser(username: string): Promise { - const url = `${usersUrl}/${encodeURIComponent(username)}`; - return await kfetch({ pathname: url }); - } - - public async deleteUser(username: string) { - const url = `${usersUrl}/${encodeURIComponent(username)}`; - await kfetch({ pathname: url, method: 'DELETE' }, {}); - } - - public async saveUser(user: EditUser) { - const url = `${usersUrl}/${encodeURIComponent(user.username)}`; - - await kfetch({ pathname: url, body: JSON.stringify(user), method: 'POST' }); - } - - public async getRoles(): Promise { - return await kfetch({ pathname: rolesUrl }); - } - - public async getRole(name: string): Promise { - const url = `${rolesUrl}/${encodeURIComponent(name)}`; - return await kfetch({ pathname: url }); - } - - public async changePassword(username: string, password: string, currentPassword: string) { - const data: Record = { - newPassword: password, - }; - if (currentPassword) { - data.password = currentPassword; - } - await kfetch({ - pathname: `${usersUrl}/${encodeURIComponent(username)}/password`, - method: 'POST', - body: JSON.stringify(data), - }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts deleted file mode 100644 index fbc0460c5908a7..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts +++ /dev/null @@ -1,47 +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 { kfetch } from 'ui/kfetch'; -import { ApiKey, ApiKeyToInvalidate } from '../../common/model'; - -interface CheckPrivilegesResponse { - areApiKeysEnabled: boolean; - isAdmin: boolean; -} - -interface InvalidateApiKeysResponse { - itemsInvalidated: ApiKeyToInvalidate[]; - errors: any[]; -} - -interface GetApiKeysResponse { - apiKeys: ApiKey[]; -} - -const apiKeysUrl = `/internal/security/api_key`; - -export class ApiKeysApi { - public static async checkPrivileges(): Promise { - return kfetch({ pathname: `${apiKeysUrl}/privileges` }); - } - - public static async getApiKeys(isAdmin: boolean = false): Promise { - const query = { - isAdmin, - }; - - return kfetch({ pathname: apiKeysUrl, query }); - } - - public static async invalidateApiKeys( - apiKeys: ApiKeyToInvalidate[], - isAdmin: boolean = false - ): Promise { - const pathname = `${apiKeysUrl}/invalidate`; - const body = JSON.stringify({ apiKeys, isAdmin }); - return kfetch({ pathname, method: 'POST', body }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.ts b/x-pack/legacy/plugins/security/public/lib/role_utils.ts deleted file mode 100644 index c33b7385306fb8..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.ts +++ /dev/null @@ -1,58 +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 { cloneDeep, get } from 'lodash'; -import { Role } from '../../common/model'; - -/** - * Returns whether given role is enabled or not - * - * @param role Object Role JSON, as returned by roles API - * @return Boolean true if role is enabled; false otherwise - */ -export function isRoleEnabled(role: Partial) { - return get(role, 'transient_metadata.enabled', true); -} - -/** - * Returns whether given role is reserved or not. - * - * @param {role} the Role as returned by roles API - */ -export function isReservedRole(role: Partial) { - return get(role, 'metadata._reserved', false); -} - -/** - * Returns whether given role is editable through the UI or not. - * - * @param role the Role as returned by roles API - */ -export function isReadOnlyRole(role: Partial): boolean { - return isReservedRole(role) || !!(role._transform_error && role._transform_error.length > 0); -} - -/** - * Returns a deep copy of the role. - * - * @param role the Role to copy. - */ -export function copyRole(role: Role) { - return cloneDeep(role); -} - -/** - * Creates a deep copy of the role suitable for cloning. - * - * @param role the Role to clone. - */ -export function prepareRoleClone(role: Role): Role { - const clone = copyRole(role); - - clone.name = ''; - - return clone; -} diff --git a/x-pack/legacy/plugins/security/public/lib/roles_api.ts b/x-pack/legacy/plugins/security/public/lib/roles_api.ts deleted file mode 100644 index 20c1491ccaac69..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/roles_api.ts +++ /dev/null @@ -1,25 +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 { kfetch } from 'ui/kfetch'; -import { Role } from '../../common/model'; - -export class RolesApi { - public static async getRoles(): Promise { - return kfetch({ pathname: '/api/security/role' }); - } - - public static async getRole(roleName: string): Promise { - return kfetch({ pathname: `/api/security/role/${encodeURIComponent(roleName)}` }); - } - - public static async deleteRole(roleName: string) { - return kfetch({ - pathname: `/api/security/role/${encodeURIComponent(roleName)}`, - method: 'DELETE', - }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts b/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts deleted file mode 100644 index 861ba530050a13..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Role, RoleIndexPrivilege } from '../../common/model'; -import { isGlobalPrivilegeDefinition } from './privilege_utils'; - -export function transformRoleForSave(role: Role, spacesEnabled: boolean) { - // Remove any placeholder index privileges - role.elasticsearch.indices = role.elasticsearch.indices.filter( - indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) - ); - - // Remove any placeholder query entries - role.elasticsearch.indices.forEach(index => index.query || delete index.query); - - // If spaces are disabled, then do not persist any space privileges - if (!spacesEnabled) { - role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); - } - - role.kibana.forEach(kibanaPrivilege => { - // If a base privilege is defined, then do not persist feature privileges - if (kibanaPrivilege.base.length > 0) { - kibanaPrivilege.feature = {}; - } - }); - - delete role.name; - delete role.transient_metadata; - delete role._unrecognized_applications; - delete role._transform_error; - - return role; -} - -function isPlaceholderPrivilege(indexPrivilege: RoleIndexPrivilege) { - return indexPrivilege.names.length === 0; -} diff --git a/x-pack/legacy/plugins/security/public/lib/util.js b/x-pack/legacy/plugins/security/public/lib/util.js deleted file mode 100644 index bdf44aa3f10bb8..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/util.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function toggle(collection, item) { - const i = collection.indexOf(item); - if (i >= 0) collection.splice(i, 1); - else collection.push(item); -} - -export function toggleSort(sort, orderBy) { - if (sort.orderBy === orderBy) sort.reverse = !sort.reverse; - else { - sort.orderBy = orderBy; - sort.reverse = false; - } -} diff --git a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts deleted file mode 100644 index 91d98782dab423..00000000000000 --- a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { IHttpResponse } from 'angular'; -import chrome from 'ui/chrome'; - -const apiBase = chrome.addBasePath(`/internal/security/fields`); - -export async function getFields($http: any, query: string): Promise { - return await $http - .get(`${apiBase}/${query}`) - .then((response: IHttpResponse) => response.data || []); -} diff --git a/x-pack/legacy/plugins/security/public/objects/lib/roles.ts b/x-pack/legacy/plugins/security/public/objects/lib/roles.ts deleted file mode 100644 index e33cbe4c6c031c..00000000000000 --- a/x-pack/legacy/plugins/security/public/objects/lib/roles.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import chrome from 'ui/chrome'; -import { Role } from '../../../common/model'; -import { copyRole } from '../../lib/role_utils'; -import { transformRoleForSave } from '../../lib/transform_role_for_save'; - -const apiBase = chrome.addBasePath(`/api/security/role`); - -export async function saveRole($http: any, role: Role, spacesEnabled: boolean) { - const data = transformRoleForSave(copyRole(role), spacesEnabled); - - return await $http.put(`${apiBase}/${role.name}`, data); -} - -export async function deleteRole($http: any, name: string) { - return await $http.delete(`${apiBase}/${name}`); -} diff --git a/x-pack/legacy/plugins/security/public/register_feature.js b/x-pack/legacy/plugins/security/public/register_feature.js deleted file mode 100644 index c0bd42690b6fdb..00000000000000 --- a/x-pack/legacy/plugins/security/public/register_feature.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'security', - title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { - defaultMessage: 'Security Settings', - }), - description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { - defaultMessage: - 'Protect your data and easily manage who has access to what with users and roles.', - }), - icon: 'securityApp', - path: '/app/kibana#/management/security', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/services/shield_indices.js b/x-pack/legacy/plugins/security/public/services/shield_indices.js deleted file mode 100644 index 791fa6cb596488..00000000000000 --- a/x-pack/legacy/plugins/security/public/services/shield_indices.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('security', []); -module.service('shieldIndices', ($http, chrome) => { - return { - getFields: query => { - return $http - .get(chrome.addBasePath(`/internal/security/fields/${query}`)) - .then(response => response.data); - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/services/shield_role.js b/x-pack/legacy/plugins/security/public/services/shield_role.js deleted file mode 100644 index 261d3449a7a2d1..00000000000000 --- a/x-pack/legacy/plugins/security/public/services/shield_role.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'angular-resource'; -import { omit } from 'lodash'; -import angular from 'angular'; -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('security', ['ngResource']); -module.service('ShieldRole', ($resource, chrome) => { - return $resource( - chrome.addBasePath('/api/security/role/:name'), - { - name: '@name', - }, - { - save: { - method: 'PUT', - transformRequest(data) { - return angular.toJson( - omit(data, 'name', 'transient_metadata', '_unrecognized_applications') - ); - }, - }, - } - ); -}); diff --git a/x-pack/legacy/plugins/security/public/views/_index.scss b/x-pack/legacy/plugins/security/public/views/_index.scss index b85a7e19973906..6c2a091adf536e 100644 --- a/x-pack/legacy/plugins/security/public/views/_index.scss +++ b/x-pack/legacy/plugins/security/public/views/_index.scss @@ -1,5 +1,2 @@ // Login styles @import './login/index'; - -// Management styles -@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/account.html b/x-pack/legacy/plugins/security/public/views/account/account.html deleted file mode 100644 index 0935c415b18295..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/account/account.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/x-pack/legacy/plugins/security/public/views/account/account.js b/x-pack/legacy/plugins/security/public/views/account/account.js index 70a7b8dce727ec..13abc44e08f965 100644 --- a/x-pack/legacy/plugins/security/public/views/account/account.js +++ b/x-pack/legacy/plugins/security/public/views/account/account.js @@ -4,17 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import routes from 'ui/routes'; -import template from './account.html'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { AccountManagementPage } from './components'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import routes from 'ui/routes'; routes.when('/account', { - template, + template: '
', k7Breadcrumbs: () => [ { text: i18n.translate('xpack.security.account.breadcrumb', { @@ -24,19 +21,15 @@ routes.when('/account', { ], controllerAs: 'accountController', controller($scope) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('userProfileReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); $scope.$$postDigest(() => { + const domNode = document.getElementById('userProfileReactRoot'); + render( - - - , - document.getElementById('userProfileReactRoot') + , + domNode ); + + $scope.$on('$destroy', () => unmountComponentAtNode(domNode)); }); }, }); diff --git a/x-pack/legacy/plugins/security/public/views/login/_index.scss b/x-pack/legacy/plugins/security/public/views/login/_index.scss index 9f133940f79777..9083c8dc3b7751 100644 --- a/x-pack/legacy/plugins/security/public/views/login/_index.scss +++ b/x-pack/legacy/plugins/security/public/views/login/_index.scss @@ -5,5 +5,4 @@ // loginChart__legend--small // loginChart__legend-isLoading -@import 'login'; - +@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/_index.scss b/x-pack/legacy/plugins/security/public/views/login/components/_index.scss new file mode 100644 index 00000000000000..a6f9598b9cc043 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/_index.scss @@ -0,0 +1 @@ +@import './login_page/index'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx index 93451453a523ab..3a970d582bdc8d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiCallOut } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; import { BasicLoginForm } from './basic_login_form'; const createMockHttp = ({ simulateError = false } = {}) => { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index e6d3b5b7536b6a..c263381fbdb564 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -9,7 +9,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; import ReactMarkdown from 'react-markdown'; import { EuiText } from '@elastic/eui'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; interface Props { http: any; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss new file mode 100644 index 00000000000000..4dd2c0cabfb5e8 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss @@ -0,0 +1 @@ +@import './login_page'; diff --git a/x-pack/legacy/plugins/security/public/views/login/_login.scss b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss similarity index 88% rename from x-pack/legacy/plugins/security/public/views/login/_login.scss rename to x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss index 607e9e6ec5e3f3..cdfad55ee064af 100644 --- a/x-pack/legacy/plugins/security/public/views/login/_login.scss +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss @@ -1,4 +1,3 @@ - .loginWelcome { @include kibanaFullScreenGraphics; } @@ -16,10 +15,6 @@ margin-bottom: $euiSizeXL; } -.loginWelcome__footerAction { - margin-right: $euiSizeS; -} - .loginWelcome__content { position: relative; margin: auto; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx index c16db007bdbdcf..a0318d50a45e58 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { LoginLayout, LoginState } from '../../../../../common/login_state'; +import { LoginLayout, LoginState } from '../../login_state'; import { LoginPage } from './login_page'; const createMockHttp = ({ simulateError = false } = {}) => { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index e7e56947ca58f8..8035789a30e9df 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -19,7 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; import classNames from 'classnames'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; import { BasicLoginForm } from '../basic_login_form'; import { DisabledLoginForm } from '../disabled_login_form'; diff --git a/x-pack/legacy/plugins/security/public/views/login/login.html b/x-pack/legacy/plugins/security/public/views/login/login.html deleted file mode 100644 index 2695fabdd63671..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/login/login.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index d9daf2d1f4d0de..0b89ac553c9a88 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -6,16 +6,14 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { parseNext } from 'plugins/security/lib/parse_next'; import { LoginPage } from 'plugins/security/views/login/components'; -// @ts-ignore -import template from 'plugins/security/views/login/login.html'; import React from 'react'; import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { parse } from 'url'; -import { LoginState } from '../../../common/login_state'; +import { parseNext } from './parse_next'; +import { LoginState } from './login_state'; const messageMap = { SESSION_EXPIRED: i18n.translate('xpack.security.login.sessionExpiredDescription', { defaultMessage: 'Your session has timed out. Please log in again.', @@ -31,7 +29,7 @@ interface AnyObject { (chrome as AnyObject) .setVisible(false) - .setRootTemplate(template) + .setRootTemplate('
') .setRootController( 'login', ( diff --git a/x-pack/legacy/plugins/security/common/login_state.ts b/x-pack/legacy/plugins/security/public/views/login/login_state.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/login_state.ts rename to x-pack/legacy/plugins/security/public/views/login/login_state.ts diff --git a/x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js b/x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js rename to x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts index 7516433c77f83d..b5e6c7dca41d8f 100644 --- a/x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js +++ b/x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { parseNext } from '../parse_next'; +import { parseNext } from './parse_next'; describe('parseNext', () => { it('should return a function', () => { - expect(parseNext).to.be.a('function'); + expect(parseNext).toBeInstanceOf(Function); }); describe('with basePath defined', () => { @@ -17,14 +16,14 @@ describe('parseNext', () => { it('should return basePath with a trailing slash when next is not specified', () => { const basePath = '/iqf'; const href = `${basePath}/login`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); it('should properly handle next without hash', () => { const basePath = '/iqf'; const next = `${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(next); + expect(parseNext(href, basePath)).toEqual(next); }); it('should properly handle next with hash', () => { @@ -32,7 +31,7 @@ describe('parseNext', () => { const next = `${basePath}/app/kibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${next}#${hash}`); + expect(parseNext(href, basePath)).toEqual(`${next}#${hash}`); }); it('should properly decode special characters', () => { @@ -40,7 +39,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(basePath)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(decodeURIComponent(`${next}#${hash}`)); + expect(parseNext(href, basePath)).toEqual(decodeURIComponent(`${next}#${hash}`)); }); // to help prevent open redirect to a different url @@ -48,7 +47,7 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = `https://example.com${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different url by abusing encodings @@ -58,7 +57,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different port @@ -66,7 +65,7 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = `http://localhost:5601${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different port by abusing encodings @@ -76,7 +75,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different base path @@ -84,18 +83,18 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = '/notbasepath/app/kibana'; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // disallow network-path references it('should return / if next is url without protocol', () => { const nextWithTwoSlashes = '//example.com'; const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`; - expect(parseNext(hrefWithTwoSlashes)).to.equal('/'); + expect(parseNext(hrefWithTwoSlashes)).toEqual('/'); const nextWithThreeSlashes = '///example.com'; const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`; - expect(parseNext(hrefWithThreeSlashes)).to.equal('/'); + expect(parseNext(hrefWithThreeSlashes)).toEqual('/'); }); }); @@ -103,34 +102,34 @@ describe('parseNext', () => { // trailing slash is important since it must match the cookie path exactly it('should return / with a trailing slash when next is not specified', () => { const href = '/login'; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); it('should properly handle next without hash', () => { const next = '/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal(next); + expect(parseNext(href)).toEqual(next); }); it('should properly handle next with hash', () => { const next = '/app/kibana'; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal(`${next}#${hash}`); + expect(parseNext(href)).toEqual(`${next}#${hash}`); }); it('should properly decode special characters', () => { const next = '%2Fapp%2Fkibana'; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal(decodeURIComponent(`${next}#${hash}`)); + expect(parseNext(href)).toEqual(decodeURIComponent(`${next}#${hash}`)); }); // to help prevent open redirect to a different url it('should return / if next includes a protocol/hostname', () => { const next = 'https://example.com/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different url by abusing encodings @@ -139,14 +138,14 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different port it('should return / if next includes a port', () => { const next = 'http://localhost:5601/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different port by abusing encodings @@ -155,18 +154,18 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // disallow network-path references it('should return / if next is url without protocol', () => { const nextWithTwoSlashes = '//example.com'; const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`; - expect(parseNext(hrefWithTwoSlashes)).to.equal('/'); + expect(parseNext(hrefWithTwoSlashes)).toEqual('/'); const nextWithThreeSlashes = '///example.com'; const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`; - expect(parseNext(hrefWithThreeSlashes)).to.equal('/'); + expect(parseNext(hrefWithThreeSlashes)).toEqual('/'); }); }); }); diff --git a/x-pack/legacy/plugins/security/public/lib/parse_next.ts b/x-pack/legacy/plugins/security/public/views/login/parse_next.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/parse_next.ts rename to x-pack/legacy/plugins/security/public/views/login/parse_next.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/_index.scss b/x-pack/legacy/plugins/security/public/views/management/_index.scss deleted file mode 100644 index 78b53845071e4a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './change_password_form/index'; -@import './edit_role/index'; -@import './edit_user/index'; -@import './role_mappings/edit_role_mapping/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html deleted file mode 100644 index e46c6f72b5d20e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js deleted file mode 100644 index e7143b10208148..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from './api_keys.html'; -import { API_KEYS_PATH } from '../management_urls'; -import { getApiKeysBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { ApiKeysGridPage } from './components'; - -routes.when(API_KEYS_PATH, { - template, - k7Breadcrumbs: getApiKeysBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('apiKeysGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts b/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts deleted file mode 100644 index 4ab7e45e848498..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts +++ /dev/null @@ -1,115 +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 { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; - -export function getUsersBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.users.breadcrumb', { - defaultMessage: 'Users', - }), - href: '#/management/security/users', - }, - ]; -} - -export function getEditUserBreadcrumbs($route: Record) { - const { username } = $route.current.params; - return [ - ...getUsersBreadcrumbs(), - { - text: username, - href: `#/management/security/users/edit/${username}`, - }, - ]; -} - -export function getCreateUserBreadcrumbs() { - return [ - ...getUsersBreadcrumbs(), - { - text: i18n.translate('xpack.security.users.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} - -export function getRolesBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.roles.breadcrumb', { - defaultMessage: 'Roles', - }), - href: '#/management/security/roles', - }, - ]; -} - -export function getEditRoleBreadcrumbs($route: Record) { - const { name } = $route.current.params; - return [ - ...getRolesBreadcrumbs(), - { - text: name, - href: `#/management/security/roles/edit/${name}`, - }, - ]; -} - -export function getCreateRoleBreadcrumbs() { - return [ - ...getUsersBreadcrumbs(), - { - text: i18n.translate('xpack.security.roles.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} - -export function getApiKeysBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.apiKeys.breadcrumb', { - defaultMessage: 'API Keys', - }), - href: '#/management/security/api_keys', - }, - ]; -} - -export function getRoleMappingBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.roleMapping.breadcrumb', { - defaultMessage: 'Role Mappings', - }), - href: '#/management/security/role_mappings', - }, - ]; -} - -export function getEditRoleMappingBreadcrumbs($route: Record) { - const { name } = $route.current.params; - return [ - ...getRoleMappingBreadcrumbs(), - { - text: - name || - i18n.translate('xpack.security.roleMappings.createBreadcrumb', { - defaultMessage: 'Create', - }), - href: `#/management/security/role_mappings/edit/${name}`, - }, - ]; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss b/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss deleted file mode 100644 index 98331c2070a31e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss +++ /dev/null @@ -1,17 +0,0 @@ -.secChangePasswordForm__panel { - max-width: $secFormWidth; -} - -.secChangePasswordForm__subLabel { - margin-bottom: $euiSizeS; -} - -.secChangePasswordForm__footer { - display: flex; - justify-content: flex-start; - align-items: center; - - .kuiButton + .kuiButton { - margin-left: $euiSizeS; - } -} diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss b/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss deleted file mode 100644 index a6058b5ddebbf0..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './change_password_form'; diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html deleted file mode 100644 index 92fb95861a6f8e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html +++ /dev/null @@ -1,141 +0,0 @@ - - -
- - - - - - -
- -
- - - - -
-
- - -
- - - - -
-
- - -
- - - - -
- - -
- - -
-
- - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js deleted file mode 100644 index d9aa59f6df1427..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from './change_password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnChangePasswordForm', function() { - return { - template, - scope: { - requireCurrentPassword: '=', - showKibanaWarning: '=', - onChangePassword: '&', - }, - restrict: 'E', - replace: true, - controllerAs: 'changePasswordController', - controller: function($scope) { - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - - this.showForm = () => { - this.isFormVisible = true; - }; - - this.hideForm = () => { - $scope.changePasswordForm.$setPristine(); - $scope.changePasswordForm.$setUntouched(); - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - }; - - this.onIncorrectPassword = () => { - this.isIncorrectPassword = true; - }; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss deleted file mode 100644 index 192091fb04e3c9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss deleted file mode 100644 index 32b3832e7a9fa9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import './collapsible_panel/collapsible_panel'; -@import './privileges/kibana/space_aware_privilege_section/index'; -@import './privileges/kibana/feature_table/index'; -@import './spaces_popover_list/spaces_popover_list'; - -.secPrivilegeFeatureIcon { - flex-shrink: 0; - margin-right: $euiSizeS; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx deleted file mode 100644 index 67c32c8393171f..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx +++ /dev/null @@ -1,716 +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 { ReactWrapper } from 'enzyme'; -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -// These modules should be moved into a common directory -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Actions } from '../../../../../../../../plugins/security/server/authorization/actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { privilegesFactory } from '../../../../../../../../plugins/security/server/authorization/privileges'; -import { RawKibanaPrivileges, Role } from '../../../../../common/model'; -import { EditRolePage } from './edit_role_page'; -import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; -import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; -import { TransformErrorSection } from './privileges/kibana/transform_error_section'; - -const buildFeatures = () => { - return [ - { - id: 'feature1', - name: 'Feature 1', - icon: 'addDataApp', - app: ['feature1App'], - privileges: { - all: { - app: ['feature1App'], - ui: ['feature1-ui'], - savedObject: { - all: [], - read: [], - }, - }, - }, - }, - { - id: 'feature2', - name: 'Feature 2', - icon: 'addDataApp', - app: ['feature2App'], - privileges: { - all: { - app: ['feature2App'], - ui: ['feature2-ui'], - savedObject: { - all: ['feature2'], - read: ['config'], - }, - }, - }, - }, - ] as Feature[]; -}; - -const buildRawKibanaPrivileges = () => { - return privilegesFactory(new Actions('unit_test_version'), { - getFeatures: () => buildFeatures(), - }).get(); -}; - -const buildBuiltinESPrivileges = () => { - return { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }; -}; - -const buildUICapabilities = (canManageSpaces = true) => { - return { - catalogue: {}, - management: {}, - navLinks: {}, - spaces: { - manage: canManageSpaces, - }, - } as UICapabilities; -}; - -const buildSpaces = () => { - return [ - { - id: 'default', - name: 'Default', - disabledFeatures: [], - _reserved: true, - }, - { - id: 'space_1', - name: 'Space 1', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - disabledFeatures: ['feature2'], - }, - ] as Space[]; -}; - -const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); -}; - -const expectSaveFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); -}; - -describe('', () => { - describe('with spaces enabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('renders an auth error when not authorized to manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(1); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); - - describe('with spaces disabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('does not care if user cannot manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(0); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx deleted file mode 100644 index 2ba012afa689dc..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { toastNotifications } from 'ui/notify'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { - KibanaPrivileges, - RawKibanaPrivileges, - Role, - BuiltinESPrivileges, -} from '../../../../../common/model'; -import { - isReadOnlyRole, - isReservedRole, - copyRole, - prepareRoleClone, -} from '../../../../lib/role_utils'; -import { deleteRole, saveRole } from '../../../../objects'; -import { ROLES_PATH } from '../../management_urls'; -import { RoleValidationResult, RoleValidator } from '../lib/validate_role'; -import { DeleteRoleButton } from './delete_role_button'; -import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; -import { ReservedRoleBadge } from './reserved_role_badge'; - -interface Props { - action: 'edit' | 'clone'; - role: Role; - runAsUsers: string[]; - indexPatterns: string[]; - httpClient: any; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - kibanaPrivileges: RawKibanaPrivileges; - builtinESPrivileges: BuiltinESPrivileges; - spaces?: Space[]; - spacesEnabled: boolean; - intl: InjectedIntl; - uiCapabilities: UICapabilities; - features: Feature[]; -} - -interface State { - role: Role; - formError: RoleValidationResult | null; -} - -class EditRolePageUI extends Component { - private validator: RoleValidator; - - constructor(props: Props) { - super(props); - - this.validator = new RoleValidator({ shouldValidate: false }); - - let role: Role; - if (props.action === 'clone') { - role = prepareRoleClone(props.role); - } else { - role = copyRole(props.role); - } - - this.state = { - role, - formError: null, - }; - } - - public componentDidMount() { - if (this.props.action === 'clone' && isReservedRole(this.props.role)) { - this.backToRoleList(); - } - } - - public render() { - const description = this.props.spacesEnabled ? ( - - ) : ( - - ); - - return ( -
- - {this.getFormTitle()} - - - - {description} - - {isReservedRole(this.state.role) && ( - - - -

- -

-
-
- )} - - - - {this.getRoleName()} - - {this.getElasticsearchPrivileges()} - - {this.getKibanaPrivileges()} - - - - {this.getFormButtons()} -
-
- ); - } - - private getFormTitle = () => { - let titleText; - const props: HTMLProps = { - tabIndex: 0, - }; - if (isReservedRole(this.state.role)) { - titleText = ( - - ); - props['aria-describedby'] = 'reservedRoleDescription'; - } else if (this.editingExistingRole()) { - titleText = ( - - ); - } else { - titleText = ( - - ); - } - - return ( - -

- {titleText} -

-
- ); - }; - - private getActionButton = () => { - if (this.editingExistingRole() && !isReadOnlyRole(this.state.role)) { - return ( - - - - ); - } - - return null; - }; - - private getRoleName = () => { - return ( - - - } - helpText={ - !isReservedRole(this.state.role) && this.editingExistingRole() ? ( - - ) : ( - undefined - ) - } - {...this.validator.validateRoleName(this.state.role)} - > - - - - ); - }; - - private onNameChange = (e: ChangeEvent) => { - const rawValue = e.target.value; - const name = rawValue.replace(/\s/g, '_'); - - this.setState({ - role: { - ...this.state.role, - name, - }, - }); - }; - - private getElasticsearchPrivileges() { - return ( -
- - -
- ); - } - - private onRoleChange = (role: Role) => { - this.setState({ - role, - }); - }; - - private getKibanaPrivileges = () => { - return ( -
- - -
- ); - }; - - private getFormButtons = () => { - if (isReadOnlyRole(this.state.role)) { - return this.getReturnToRoleListButton(); - } - - return ( - - {this.getSaveButton()} - {this.getCancelButton()} - - {this.getActionButton()} - - ); - }; - - private getReturnToRoleListButton = () => { - return ( - - - - ); - }; - - private getSaveButton = () => { - const saveText = this.editingExistingRole() ? ( - - ) : ( - - ); - - return ( - - {saveText} - - ); - }; - - private getCancelButton = () => { - return ( - - - - ); - }; - - private editingExistingRole = () => { - return !!this.props.role.name && this.props.action === 'edit'; - }; - - private saveRole = () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.role); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - - const { httpClient, intl, spacesEnabled } = this.props; - - saveRole(httpClient, this.state.role, spacesEnabled) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', - defaultMessage: 'Saved role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - } - }; - - private handleDeleteRole = () => { - const { httpClient, role, intl } = this.props; - - deleteRole(httpClient, role.name) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - }; - - private backToRoleList = () => { - window.location.hash = ROLES_PATH; - }; -} - -export const EditRolePage = injectI18n(EditRolePageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx deleted file mode 100644 index 5ba3d1daf61acb..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; -import { ClusterPrivileges } from './cluster_privileges'; -import { ElasticsearchPrivileges } from './elasticsearch_privileges'; -import { IndexPrivileges } from './index_privileges'; - -test('it renders without crashing', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ClusterPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(ClusterPrivileges)).toHaveLength(1); -}); - -test('it renders IndexPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(IndexPrivileges)).toHaveLength(1); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html b/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html deleted file mode 100644 index ca4073dcad6f5c..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js deleted file mode 100644 index 27c9beb4ba8284..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import routes from 'ui/routes'; -import { capabilities } from 'ui/capabilities'; -import { kfetch } from 'ui/kfetch'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import template from 'plugins/security/views/management/edit_role/edit_role.html'; -import 'plugins/security/services/shield_role'; -import 'plugins/security/services/shield_indices'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { UserAPIClient } from '../../../lib/api'; -import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls'; -import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs'; - -import { EditRolePage } from './components'; - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { i18n } from '@kbn/i18n'; - -const routeDefinition = action => ({ - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - action === 'edit' && $route.current.params.name - ? getEditRoleBreadcrumbs - : getCreateRoleBreadcrumbs - ), - resolve: { - role($route, ShieldRole, Promise, kbnUrl) { - const name = $route.current.params.name; - - let role; - - if (name != null) { - role = ShieldRole.get({ name }).$promise.catch(response => { - if (response.status === 404) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.security.management.roles.roleNotFound', { - defaultMessage: 'No "{roleName}" role found.', - values: { roleName: name }, - }), - }); - kbnUrl.redirect(ROLES_PATH); - } else { - return fatalError(response); - } - }); - } else { - role = Promise.resolve( - new ShieldRole({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _unrecognized_applications: [], - }) - ); - } - - return role.then(res => res.toJSON()); - }, - users() { - return new UserAPIClient().getUsers().then(users => _.map(users, 'username')); - }, - indexPatterns() { - return npStart.plugins.data.indexPatterns.getTitles(); - }, - spaces(spacesEnabled) { - if (spacesEnabled) { - return kfetch({ method: 'get', pathname: '/api/spaces/space' }); - } - return []; - }, - kibanaPrivileges() { - return kfetch({ - method: 'get', - pathname: '/api/security/privileges', - query: { includeActions: true }, - }); - }, - builtinESPrivileges() { - return kfetch({ method: 'get', pathname: '/internal/security/esPrivileges/builtin' }); - }, - features() { - return kfetch({ method: 'get', pathname: '/api/features' }).catch(e => { - // TODO: This check can be removed once all of these `resolve` entries are moved out of Angular and into the React app. - const unauthorizedForFeatures = _.get(e, 'body.statusCode') === 404; - if (unauthorizedForFeatures) { - return []; - } - throw e; - }); - }, - }, - controllerAs: 'editRole', - controller($injector, $scope, $http, enableSpaceAwarePrivileges) { - const $route = $injector.get('$route'); - const role = $route.current.locals.role; - - const allowDocumentLevelSecurity = xpackInfo.get( - 'features.security.allowRoleDocumentLevelSecurity' - ); - const allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity'); - if (role.elasticsearch.indices.length === 0) { - const emptyOption = { - names: [], - privileges: [], - }; - - if (allowFieldLevelSecurity) { - emptyOption.field_security = { - grant: ['*'], - except: [], - }; - } - - if (allowDocumentLevelSecurity) { - emptyOption.query = ''; - } - - role.elasticsearch.indices.push(emptyOption); - } - - const { - users, - indexPatterns, - spaces, - kibanaPrivileges, - builtinESPrivileges, - features, - } = $route.current.locals; - - $scope.$$postDigest(async () => { - const domNode = document.getElementById('editRoleReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); - -routes.when(`${CLONE_ROLES_PATH}/:name`, routeDefinition('clone')); -routes.when(`${EDIT_ROLES_PATH}/:name?`, routeDefinition('edit')); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss deleted file mode 100644 index c5da74aa3f785c..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html deleted file mode 100644 index 4fa2768480874a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js deleted file mode 100644 index ab218022c6ee64..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js +++ /dev/null @@ -1,58 +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 routes from 'ui/routes'; -import template from 'plugins/security/views/management/edit_user/edit_user.html'; -import 'angular-resource'; -import 'ui/angular_ui_select'; -import 'plugins/security/services/shield_role'; -import { EDIT_USERS_PATH } from '../management_urls'; -import { EditUserPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { getEditUserBreadcrumbs, getCreateUserBreadcrumbs } from '../breadcrumbs'; - -const renderReact = (elem, changeUrl, username) => { - render( - - - , - elem - ); -}; - -routes.when(`${EDIT_USERS_PATH}/:username?`, { - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - $route.current.params.username ? getEditUserBreadcrumbs : getCreateUserBreadcrumbs - ), - controllerAs: 'editUser', - controller($scope, $route, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('editUserReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('editUserReactRoot'); - const username = $route.current.params.username; - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, changeUrl, username); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/management.js b/x-pack/legacy/plugins/security/public/views/management/management.js deleted file mode 100644 index f0369f232aeba8..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/management.js +++ /dev/null @@ -1,134 +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 'plugins/security/views/management/change_password_form/change_password_form'; -import 'plugins/security/views/management/password_form/password_form'; -import 'plugins/security/views/management/users_grid/users'; -import 'plugins/security/views/management/roles_grid/roles'; -import 'plugins/security/views/management/api_keys_grid/api_keys'; -import 'plugins/security/views/management/edit_user/edit_user'; -import 'plugins/security/views/management/edit_role/index'; -import 'plugins/security/views/management/role_mappings/role_mappings_grid'; -import 'plugins/security/views/management/role_mappings/edit_role_mapping'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { ROLES_PATH, USERS_PATH, API_KEYS_PATH, ROLE_MAPPINGS_PATH } from './management_urls'; - -import { management } from 'ui/management'; -import { npSetup } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - -routes - .defaults(/^\/management\/security(\/|$)/, { - resolve: { - showLinks(kbnUrl, Promise) { - if (!xpackInfo.get('features.security.showLinks')) { - toastNotifications.addDanger({ - title: xpackInfo.get('features.security.linksMessage'), - }); - kbnUrl.redirect('/management'); - return Promise.halt(); - } - }, - }, - }) - .defaults(/\/management/, { - resolve: { - securityManagementSection: function() { - const showSecurityLinks = xpackInfo.get('features.security.showLinks'); - const showRoleMappingsManagementLink = xpackInfo.get( - 'features.security.showRoleMappingsManagement' - ); - - function deregisterSecurity() { - management.deregister('security'); - } - - function deregisterRoleMappingsManagement() { - if (management.hasItem('security')) { - const security = management.getSection('security'); - if (security.hasItem('roleMappings')) { - security.deregister('roleMappings'); - } - } - } - - function ensureSecurityRegistered() { - const registerSecurity = () => - management.register('security', { - display: i18n.translate('xpack.security.management.securityTitle', { - defaultMessage: 'Security', - }), - order: 100, - icon: 'securityApp', - }); - const getSecurity = () => management.getSection('security'); - - const security = management.hasItem('security') ? getSecurity() : registerSecurity(); - - if (!security.hasItem('users')) { - security.register('users', { - name: 'securityUsersLink', - order: 10, - display: i18n.translate('xpack.security.management.usersTitle', { - defaultMessage: 'Users', - }), - url: `#${USERS_PATH}`, - }); - } - - if (!security.hasItem('roles')) { - security.register('roles', { - name: 'securityRolesLink', - order: 20, - display: i18n.translate('xpack.security.management.rolesTitle', { - defaultMessage: 'Roles', - }), - url: `#${ROLES_PATH}`, - }); - } - - if (!security.hasItem('apiKeys')) { - security.register('apiKeys', { - name: 'securityApiKeysLink', - order: 30, - display: i18n.translate('xpack.security.management.apiKeysTitle', { - defaultMessage: 'API Keys', - }), - url: `#${API_KEYS_PATH}`, - }); - } - - if (showRoleMappingsManagementLink && !security.hasItem('roleMappings')) { - security.register('roleMappings', { - name: 'securityRoleMappingLink', - order: 30, - display: i18n.translate('xpack.security.management.roleMappingsTitle', { - defaultMessage: 'Role Mappings', - }), - url: `#${ROLE_MAPPINGS_PATH}`, - }); - } - } - - if (!showSecurityLinks) { - deregisterSecurity(); - } else { - if (!showRoleMappingsManagementLink) { - deregisterRoleMappingsManagement(); - } - - // getCurrentUser will reject if there is no authenticated user, so we prevent them from - // seeing the security management screens. - return npSetup.plugins.security.authc - .getCurrentUser() - .then(ensureSecurityRegistered) - .catch(deregisterSecurity); - } - }, - }, - }); diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html deleted file mode 100644 index 72956992100f5e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html +++ /dev/null @@ -1,53 +0,0 @@ - - -
- - - - -
-
- - -
- - - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js deleted file mode 100644 index edcccdb5e6e697..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from './password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnPasswordForm', function() { - return { - template, - scope: { - password: '=', - }, - restrict: 'E', - replace: true, - controllerAs: 'passwordController', - controller: function() { - this.confirmation = null; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss deleted file mode 100644 index 80e08ebcf12267..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/rule_editor_panel/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx deleted file mode 100644 index 375a8d9f374a86..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from 'test_utils/find_test_subject'; - -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import 'test_utils/stub_web_worker'; - -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { EditRoleMappingPage } from '.'; -import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../../components'; -import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; -import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; -import { EuiComboBox } from '@elastic/eui'; - -jest.mock('../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); - -describe('EditRoleMappingPage', () => { - it('allows a role mapping to be created', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { - target: { value: 'my-role-mapping' }, - }); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'my-role-mapping', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - all: [{ field: { username: '*' } }], - }, - metadata: {}, - }); - }); - - it('allows a role mapping to be updated', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - wrapper.find('button[id="addRuleOption"]').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'foo', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - any: [ - { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, - { field: { username: '*' } }, - ], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { source: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: false, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - }); - - it('renders the visual editor by default for simple rule sets', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: { - all: [ - { - field: { - username: '*', - }, - }, - { - field: { - dn: null, - }, - }, - { - field: { - realm: ['ldap', 'pki', null, 12], - }, - }, - ], - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); - }); - - it('renders the JSON editor by default for complex rule sets', async () => { - const createRule = (depth: number): Record => { - if (depth > 0) { - const rule = { - all: [ - { - field: { - username: '*', - }, - }, - ], - } as Record; - - const subRule = createRule(depth - 1); - if (subRule) { - rule.all.push(subRule); - } - - return rule; - } - return null as any; - }; - - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: createRule(10), - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html deleted file mode 100644 index ca8ab9c35c49ba..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx deleted file mode 100644 index b064a4dc50a228..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './edit_role_mapping.html'; -import { CREATE_ROLE_MAPPING_PATH } from '../../management_urls'; -import { getEditRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { EditRoleMappingPage } from './components'; - -routes.when(`${CREATE_ROLE_MAPPING_PATH}/:name?`, { - template, - k7Breadcrumbs: getEditRoleMappingBreadcrumbs, - controller($scope, $route) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('editRoleMappingReactRoot'); - - const { name } = $route.current.params; - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx deleted file mode 100644 index 259cdc71e25a22..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { RoleMappingsGridPage } from '.'; -import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../../components'; -import { EmptyPrompt } from './empty_prompt'; -import { findTestSubject } from 'test_utils/find_test_subject'; -import { EuiLink } from '@elastic/eui'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { act } from '@testing-library/react'; - -describe('RoleMappingsGridPage', () => { - it('renders an empty prompt when no role mappings exist', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(EmptyPrompt)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(EmptyPrompt)).toHaveLength(1); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: [], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders links to mapped roles', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); - expect(links).toHaveLength(1); - expect(links.at(0).props()).toMatchObject({ - href: '#/management/security/roles/edit/superuser', - }); - }); - - it('describes the number of mapped role templates', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - role_templates: [{}, {}], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const templates = findTestSubject(wrapper, 'roleMappingRoles'); - expect(templates).toHaveLength(1); - expect(templates.text()).toEqual(`2 role templates defined`); - }); - - it('allows role mappings to be deleted, refreshing the grid after', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some-realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'some-realm', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); - expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); - - findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); - expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); - - await act(async () => { - findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); - await nextTick(); - wrapper.update(); - }); - - expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); - // Expect an additional API call to refresh the grid - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx deleted file mode 100644 index 9e925d0fa6dc0a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './role_mappings.html'; -import { ROLE_MAPPINGS_PATH } from '../../management_urls'; -import { getRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { RoleMappingsGridPage } from './components'; - -routes.when(ROLE_MAPPINGS_PATH, { - template, - k7Breadcrumbs: getRoleMappingBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('roleMappingsGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html deleted file mode 100644 index cff3b821d132c0..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html deleted file mode 100644 index 0552b655afafd9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js deleted file mode 100644 index e9c42824711b31..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/roles_grid/roles.html'; -import { ROLES_PATH } from '../management_urls'; -import { getRolesBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { RolesGridPage } from './components'; - -routes.when(ROLES_PATH, { - template, - k7Breadcrumbs: getRolesBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('rolesGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html deleted file mode 100644 index 3dce7326d001ae..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js deleted file mode 100644 index 8d4e0526251d76..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/users_grid/users.html'; -import { SECURITY_PATH, USERS_PATH } from '../management_urls'; -import { UsersListPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import { I18nContext } from 'ui/i18n'; -import { getUsersBreadcrumbs } from '../breadcrumbs'; - -routes.when(SECURITY_PATH, { - redirectTo: USERS_PATH, -}); - -const renderReact = (elem, changeUrl) => { - render( - - - , - elem - ); -}; - -routes.when(USERS_PATH, { - template, - k7Breadcrumbs: getUsersBreadcrumbs, - controller($scope, $http, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('usersReactRoot'); - if (elem) unmountComponentAtNode(elem); - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('usersReactRoot'); - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, $http, changeUrl); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx index fb39c517e1c2ca..4c79c499cc0e6a 100644 --- a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx +++ b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx @@ -11,8 +11,7 @@ import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { npSetup } from 'ui/new_platform'; -import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; -import { AuthenticatedUser } from '../../../common/model'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../../../../plugins/security/public'; import { AuthenticationStatePage } from '../../components/authentication_state_page'; chrome diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index afeb8c3c13a4fd..142729189e49b3 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -99,7 +99,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network/ip/127.0.0.1?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -107,7 +107,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network/ip/127.0.0.1?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/ip/127.0.0.1/source?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); diff --git a/x-pack/legacy/plugins/siem/default_index_pattern.ts b/x-pack/legacy/plugins/siem/default_index_pattern.ts index 6719e245e0289c..4d53aeb000c557 100644 --- a/x-pack/legacy/plugins/siem/default_index_pattern.ts +++ b/x-pack/legacy/plugins/siem/default_index_pattern.ts @@ -6,6 +6,7 @@ /** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ export const defaultIndexPattern = [ + 'apm-*-transaction*', 'auditbeat-*', 'endgame-*', 'filebeat-*', diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index edbb62feb580f8..f6f2ead2d64fac 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -32,6 +32,7 @@ import { } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; import { initServerWithKibana } from './server/kibana.index'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const siem = (kibana: any) => { @@ -62,6 +63,7 @@ export const siem = (kibana: any) => { order: 9000, title: APP_NAME, url: `/app/${APP_ID}`, + category: DEFAULT_APP_CATEGORIES.security, }, ], uiSettingDefaults: { diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx index 87d83f7f2972c1..0b99a8b059df79 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx @@ -16,11 +16,11 @@ import { MatrixHistogramGqlQuery } from '../../containers/matrix_histogram/index const ID = 'alertsOverTimeQuery'; export const alertsStackByOptions: MatrixHistogramOption[] = [ { - text: i18n.CATEGORY, + text: 'event.category', value: 'event.category', }, { - text: i18n.MODULE, + text: 'event.module', value: 'event.module', }, ]; @@ -54,7 +54,6 @@ export const AlertsView = ({ <> diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts index 8c6248e38c057d..408c406a854be4 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts @@ -14,10 +14,14 @@ export const TOTAL_COUNT_OF_ALERTS = i18n.translate('xpack.siem.alertsView.total defaultMessage: 'alerts match the search criteria', }); -export const ALERTS_TABLE_TITLE = i18n.translate('xpack.siem.alertsView.alertsDocumentType', { +export const ALERTS_TABLE_TITLE = i18n.translate('xpack.siem.alertsView.alertsTableTitle', { defaultMessage: 'Alerts', }); +export const ALERTS_GRAPH_TITLE = i18n.translate('xpack.siem.alertsView.alertsGraphTitle', { + defaultMessage: 'Alert detection frequency', +}); + export const ALERTS_STACK_BY_MODULE = i18n.translate( 'xpack.siem.alertsView.alertsStackByOptions.module', { diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx index 86d003bf577f3a..4d92e8cb1335d4 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx @@ -8,10 +8,10 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { ThemeProvider } from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../../src/plugins/data/public'; import { SuggestionItem } from '../suggestion_item'; -const suggestion: AutocompleteSuggestion = { +const suggestion: autocomplete.QuerySuggestion = { description: 'Description...', end: 3, start: 1, diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx index 27e87d25e286f5..ef16f79a4b83cf 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx @@ -10,13 +10,13 @@ import { mount, shallow } from 'enzyme'; import { noop } from 'lodash/fp'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import { TestProviders } from '../../mock'; import { AutocompleteField } from '.'; -const mockAutoCompleteData: AutocompleteSuggestion[] = [ +const mockAutoCompleteData: autocomplete.QuerySuggestion[] = [ { type: 'field', text: 'agent.ephemeral_id ', diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx index 124ef26602f35c..2f76ae21944be7 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx @@ -11,7 +11,7 @@ import { EuiPanel, } from '@elastic/eui'; import React from 'react'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; import euiStyled from '../../../../../common/eui_styled_components'; @@ -25,7 +25,7 @@ interface AutocompleteFieldProps { onSubmit?: (value: string) => void; onChange?: (value: string) => void; placeholder?: string; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; value: string; } diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx index aaf7be2f7f5a6d..44bc65bb0dc15d 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx @@ -9,13 +9,13 @@ import { transparentize } from 'polished'; import React from 'react'; import styled from 'styled-components'; import euiStyled from '../../../../../common/eui_styled_components'; -import { AutocompleteSuggestion } from '../../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; onClick?: React.MouseEventHandler; onMouseEnter?: React.MouseEventHandler; - suggestion: AutocompleteSuggestion; + suggestion: autocomplete.QuerySuggestion; } export const SuggestionItem = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap index 666a8249c27d84..07cbd6dfe03706 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap @@ -365,6 +365,7 @@ exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] = "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index 2a3e69787c05c2..1f06385e12c942 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -5,11 +5,16 @@ */ import { IndexPatternMapping } from '../types'; +import { IndexPatternSavedObject } from '../../ml_popover/types'; export const mockIndexPatternIds: IndexPatternMapping[] = [ { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, ]; +export const mockAPMIndexPatternIds: IndexPatternMapping[] = [ + { title: 'apm-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, +]; + export const mockSourceLayer = { sourceDescriptor: { id: 'uuid.v4()', @@ -113,6 +118,109 @@ export const mockDestinationLayer = { query: { query: '', language: 'kuery' }, }; +export const mockClientLayer = { + sourceDescriptor: { + id: 'uuid.v4()', + type: 'ES_SEARCH', + applyGlobalQuery: true, + geoField: 'client.geo.location', + filterByMapBounds: false, + tooltipProperties: [ + 'host.name', + 'client.ip', + 'client.domain', + 'client.geo.country_iso_code', + 'client.as.organization.name', + ], + useTopHits: false, + topHitsTimeField: '@timestamp', + topHitsSize: 1, + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#6092C0' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#FFFFFF' }, + }, + lineWidth: { type: 'STATIC', options: { size: 2 } }, + iconSize: { type: 'STATIC', options: { size: 8 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbol: { + options: { symbolizeAs: 'icon', symbolId: 'home' }, + }, + }, + }, + id: 'uuid.v4()', + label: `apm-* | Client Point`, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, + joins: [], +}; + +export const mockServerLayer = { + sourceDescriptor: { + id: 'uuid.v4()', + type: 'ES_SEARCH', + applyGlobalQuery: true, + geoField: 'server.geo.location', + filterByMapBounds: true, + tooltipProperties: [ + 'host.name', + 'server.ip', + 'server.domain', + 'server.geo.country_iso_code', + 'server.as.organization.name', + ], + useTopHits: false, + topHitsTimeField: '@timestamp', + topHitsSize: 1, + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#D36086' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#FFFFFF' }, + }, + lineWidth: { type: 'STATIC', options: { size: 2 } }, + iconSize: { type: 'STATIC', options: { size: 8 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbol: { + options: { symbolizeAs: 'icon', symbolId: 'marker' }, + }, + }, + }, + id: 'uuid.v4()', + label: `apm-* | Server Point`, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, +}; + export const mockLineLayer = { sourceDescriptor: { type: 'ES_PEW_PEW', @@ -173,6 +281,66 @@ export const mockLineLayer = { query: { query: '', language: 'kuery' }, }; +export const mockClientServerLineLayer = { + sourceDescriptor: { + type: 'ES_PEW_PEW', + applyGlobalQuery: true, + id: 'uuid.v4()', + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + sourceGeoField: 'client.geo.location', + destGeoField: 'server.geo.location', + metrics: [ + { type: 'sum', field: 'client.bytes', label: 'client.bytes' }, + { type: 'sum', field: 'server.bytes', label: 'server.bytes' }, + ], + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#1EA593' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#6092C0' }, + }, + lineWidth: { + type: 'DYNAMIC', + options: { + field: { + label: 'count', + name: 'doc_count', + origin: 'source', + }, + minSize: 1, + maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + }, + }, + iconSize: { type: 'STATIC', options: { size: 10 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbol: { + options: { symbolizeAs: 'circle', symbolId: 'airfield' }, + }, + }, + }, + id: 'uuid.v4()', + label: `apm-* | Line`, + minZoom: 0, + maxZoom: 24, + alpha: 0.5, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, +}; + export const mockLayerList = [ { sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, @@ -209,3 +377,83 @@ export const mockLayerListDouble = [ mockDestinationLayer, mockSourceLayer, ]; + +export const mockLayerListMixed = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'uuid.v4()', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: null, + type: 'VECTOR_TILE', + }, + mockLineLayer, + mockDestinationLayer, + mockSourceLayer, + mockClientServerLineLayer, + mockServerLayer, + mockClientLayer, +]; + +export const mockAPMIndexPattern: IndexPatternSavedObject = { + id: 'apm-*', + type: 'index-pattern', + updated_at: '', + version: 'abc', + attributes: { + title: 'apm-*', + }, +}; + +export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { + id: 'apm-7.*', + type: 'index-pattern', + updated_at: '', + version: 'abc', + attributes: { + title: 'apm-7.*', + }, +}; + +export const mockFilebeatIndexPattern: IndexPatternSavedObject = { + id: 'filebeat-*', + type: 'index-pattern', + updated_at: '', + version: 'abc', + attributes: { + title: 'filebeat-*', + }, +}; + +export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { + id: 'auditbeat-*', + type: 'index-pattern', + updated_at: '', + version: 'abc', + attributes: { + title: 'auditbeat-*', + }, +}; + +export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { + id: 'apm-*-transaction*', + type: 'index-pattern', + updated_at: '', + version: 'abc', + attributes: { + title: 'apm-*-transaction*', + }, +}; + +export const mockGlobIndexPattern: IndexPatternSavedObject = { + id: '*', + type: 'index-pattern', + updated_at: '', + version: 'abc', + attributes: { + title: '*', + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index 771e220a2a0b3e..cbbb4f8c6249eb 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -18,7 +18,7 @@ import { Loader } from '../loader'; import { displayErrorToast, useStateToaster } from '../toasters'; import { Embeddable } from './embeddable'; import { EmbeddableHeader } from './embeddable_header'; -import { createEmbeddable } from './embedded_map_helpers'; +import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; @@ -107,10 +107,12 @@ export const EmbeddedMapComponent = ({ useEffect(() => { let isSubscribed = true; async function setupEmbeddable() { - // Ensure at least one `siem:defaultIndex` index pattern exists before trying to import - const matchingIndexPatterns = kibanaIndexPatterns.filter(ip => - siemDefaultIndices.includes(ip.attributes.title) - ); + // Ensure at least one `siem:defaultIndex` kibana index pattern exists before creating embeddable + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns, + siemDefaultIndices, + }); + if (matchingIndexPatterns.length === 0 && isSubscribed) { setIsLoading(false); setIsIndexError(true); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx index a83e8377deeb68..0ffb13cd660286 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx @@ -4,9 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createEmbeddable } from './embedded_map_helpers'; +import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; import { createPortalNode } from 'react-reverse-portal'; +import { + mockAPMIndexPattern, + mockAPMRegexIndexPattern, + mockAPMTransactionIndexPattern, + mockAuditbeatIndexPattern, + mockFilebeatIndexPattern, + mockGlobIndexPattern, +} from './__mocks__/mock'; jest.mock('ui/new_platform'); @@ -51,4 +59,58 @@ describe('embedded_map_helpers', () => { expect(embeddable.reload).toHaveBeenCalledTimes(1); }); }); + + describe('findMatchingIndexPatterns', () => { + const siemDefaultIndices = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ]; + + test('finds exact matching index patterns ', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [mockFilebeatIndexPattern, mockAuditbeatIndexPattern], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern, mockAuditbeatIndexPattern]); + }); + + test('finds glob-matched index patterns ', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [mockAPMIndexPattern, mockFilebeatIndexPattern], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([mockAPMIndexPattern, mockFilebeatIndexPattern]); + }); + + test('does not find glob-matched index pattern containing regex', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [mockAPMRegexIndexPattern, mockFilebeatIndexPattern], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]); + }); + + test('finds exact glob-matched index patterns ', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [mockAPMTransactionIndexPattern, mockFilebeatIndexPattern], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockAPMTransactionIndexPattern, + mockFilebeatIndexPattern, + ]); + }); + + test('finds glob-only index patterns ', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [mockGlobIndexPattern, mockFilebeatIndexPattern], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([mockGlobIndexPattern, mockFilebeatIndexPattern]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 838e74cc5624c8..2d4714401f3b31 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -7,6 +7,7 @@ import uuid from 'uuid'; import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; +import minimatch from 'minimatch'; import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { IndexPatternMapping, @@ -20,6 +21,7 @@ import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; +import { IndexPatternSavedObject } from '../ml_popover/types'; /** * Creates MapEmbeddable with provided initial configuration @@ -108,3 +110,25 @@ export const createEmbeddable = async ( return embeddableObject; }; + +/** + * Returns kibanaIndexPatterns that wildcard match at least one of siemDefaultIndices + * + * @param kibanaIndexPatterns + * @param siemDefaultIndices + */ +export const findMatchingIndexPatterns = ({ + kibanaIndexPatterns, + siemDefaultIndices, +}: { + kibanaIndexPatterns: IndexPatternSavedObject[]; + siemDefaultIndices: string[]; +}): IndexPatternSavedObject[] => { + try { + return kibanaIndexPatterns.filter(kip => + siemDefaultIndices.some(sdi => minimatch(sdi, kip.attributes.title)) + ); + } catch { + return []; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts index 4004db5f21795c..1707293ff6fd8e 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getDestinationLayer, getLayerList, getLineLayer, getSourceLayer } from './map_config'; +import { getDestinationLayer, getLayerList, getLineLayer, getSourceLayer, lmc } from './map_config'; import { + mockAPMIndexPatternIds, + mockClientLayer, + mockClientServerLineLayer, mockDestinationLayer, mockIndexPatternIds, mockLayerList, mockLayerListDouble, + mockLayerListMixed, mockLineLayer, + mockServerLayer, mockSourceLayer, } from './__mocks__/mock'; @@ -32,29 +37,70 @@ describe('map_config', () => { const layerList = getLayerList([...mockIndexPatternIds, ...mockIndexPatternIds]); expect(layerList).toStrictEqual(mockLayerListDouble); }); + + test('it returns the complete layerList for multiple indices with custom layer mapping', () => { + const layerList = getLayerList([...mockIndexPatternIds, ...mockAPMIndexPatternIds]); + expect(layerList).toStrictEqual(mockLayerListMixed); + }); }); describe('#getSourceLayer', () => { test('it returns a source layer', () => { - const layerList = getSourceLayer(mockIndexPatternIds[0].title, mockIndexPatternIds[0].id); + const layerList = getSourceLayer( + mockIndexPatternIds[0].title, + mockIndexPatternIds[0].id, + lmc.default.source + ); expect(layerList).toStrictEqual(mockSourceLayer); }); + + test('it returns a source layer for custom layer mapping', () => { + const layerList = getSourceLayer( + mockAPMIndexPatternIds[0].title, + mockAPMIndexPatternIds[0].id, + lmc[mockAPMIndexPatternIds[0].title].source + ); + expect(layerList).toStrictEqual(mockClientLayer); + }); }); describe('#getDestinationLayer', () => { test('it returns a destination layer', () => { const layerList = getDestinationLayer( mockIndexPatternIds[0].title, - mockIndexPatternIds[0].id + mockIndexPatternIds[0].id, + lmc.default.destination ); expect(layerList).toStrictEqual(mockDestinationLayer); }); + + test('it returns a destination layer for custom layer mapping', () => { + const layerList = getDestinationLayer( + mockAPMIndexPatternIds[0].title, + mockAPMIndexPatternIds[0].id, + lmc[mockAPMIndexPatternIds[0].title].destination + ); + expect(layerList).toStrictEqual(mockServerLayer); + }); }); describe('#getLineLayer', () => { test('it returns a line layer', () => { - const layerList = getLineLayer(mockIndexPatternIds[0].title, mockIndexPatternIds[0].id); + const layerList = getLineLayer( + mockIndexPatternIds[0].title, + mockIndexPatternIds[0].id, + lmc.default + ); expect(layerList).toStrictEqual(mockLineLayer); }); + + test('it returns a line layer for custom layer mapping', () => { + const layerList = getLineLayer( + mockAPMIndexPatternIds[0].title, + mockAPMIndexPatternIds[0].id, + lmc[mockAPMIndexPatternIds[0].title] + ); + expect(layerList).toStrictEqual(mockClientServerLineLayer); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index f2d4505ffd1bf2..f34376421e9b2d 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -6,11 +6,16 @@ import uuid from 'uuid'; import { euiPaletteColorBlind } from '@elastic/eui'; -import { IndexPatternMapping } from './types'; +import { + IndexPatternMapping, + LayerMapping, + LayerMappingCollection, + LayerMappingDetails, +} from './types'; import * as i18n from './translations'; const euiVisColorPalette = euiPaletteColorBlind(); -// Update source/destination field mappings to modify what fields will be returned to map tooltip +// Update field mappings to modify what fields will be returned to map tooltip const sourceFieldMappings: Record = { 'host.name': i18n.HOST, 'source.ip': i18n.SOURCE_IP, @@ -25,16 +30,67 @@ const destinationFieldMappings: Record = { 'destination.geo.country_iso_code': i18n.LOCATION, 'destination.as.organization.name': i18n.ASN, }; +const clientFieldMappings: Record = { + 'host.name': i18n.HOST, + 'client.ip': i18n.CLIENT_IP, + 'client.domain': i18n.CLIENT_DOMAIN, + 'client.geo.country_iso_code': i18n.LOCATION, + 'client.as.organization.name': i18n.ASN, +}; +const serverFieldMappings: Record = { + 'host.name': i18n.HOST, + 'server.ip': i18n.SERVER_IP, + 'server.domain': i18n.SERVER_DOMAIN, + 'server.geo.country_iso_code': i18n.LOCATION, + 'server.as.organization.name': i18n.ASN, +}; // Mapping of field -> display name for use within map tooltip export const sourceDestinationFieldMappings: Record = { ...sourceFieldMappings, ...destinationFieldMappings, + ...clientFieldMappings, + ...serverFieldMappings, }; // Field names of LineLayer props returned from Maps API export const SUM_OF_SOURCE_BYTES = 'sum_of_source.bytes'; export const SUM_OF_DESTINATION_BYTES = 'sum_of_destination.bytes'; +export const SUM_OF_CLIENT_BYTES = 'sum_of_client.bytes'; +export const SUM_OF_SERVER_BYTES = 'sum_of_server.bytes'; + +// Mapping to fields for creating specific layers for a given index pattern +// e.g. The apm-* index pattern needs layers for client/server instead of source/destination +export const lmc: LayerMappingCollection = { + default: { + source: { + metricField: 'source.bytes', + geoField: 'source.geo.location', + tooltipProperties: Object.keys(sourceFieldMappings), + label: i18n.SOURCE_LAYER, + }, + destination: { + metricField: 'destination.bytes', + geoField: 'destination.geo.location', + tooltipProperties: Object.keys(destinationFieldMappings), + label: i18n.DESTINATION_LAYER, + }, + }, + 'apm-*': { + source: { + metricField: 'client.bytes', + geoField: 'client.geo.location', + tooltipProperties: Object.keys(clientFieldMappings), + label: i18n.CLIENT_LAYER, + }, + destination: { + metricField: 'server.bytes', + geoField: 'server.geo.location', + tooltipProperties: Object.keys(serverFieldMappings), + label: i18n.SERVER_LAYER, + }, + }, +}; /** * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, @@ -58,9 +114,9 @@ export const getLayerList = (indexPatternIds: IndexPatternMapping[]) => { ...indexPatternIds.reduce((acc: object[], { title, id }) => { return [ ...acc, - getLineLayer(title, id), - getDestinationLayer(title, id), - getSourceLayer(title, id), + getLineLayer(title, id, lmc[title] ?? lmc.default), + getDestinationLayer(title, id, lmc[title]?.destination ?? lmc.default.destination), + getSourceLayer(title, id, lmc[title]?.source ?? lmc.default.source), ]; }, []), ]; @@ -72,15 +128,20 @@ export const getLayerList = (indexPatternIds: IndexPatternMapping[]) => { * * @param indexPatternTitle used as layer name in LayerToC UI: "${indexPatternTitle} | Source point" * @param indexPatternId used as layer's indexPattern to query for data + * @param layerDetails layer-specific field details */ -export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string) => ({ +export const getSourceLayer = ( + indexPatternTitle: string, + indexPatternId: string, + layerDetails: LayerMappingDetails +) => ({ sourceDescriptor: { id: uuid.v4(), type: 'ES_SEARCH', applyGlobalQuery: true, - geoField: 'source.geo.location', + geoField: layerDetails.geoField, filterByMapBounds: false, - tooltipProperties: Object.keys(sourceFieldMappings), + tooltipProperties: layerDetails.tooltipProperties, useTopHits: false, topHitsTimeField: '@timestamp', topHitsSize: 1, @@ -109,7 +170,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string }, }, id: uuid.v4(), - label: `${indexPatternTitle} | ${i18n.SOURCE_LAYER}`, + label: `${indexPatternTitle} | ${layerDetails.label}`, minZoom: 0, maxZoom: 24, alpha: 1, @@ -125,15 +186,21 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string * * @param indexPatternTitle used as layer name in LayerToC UI: "${indexPatternTitle} | Destination point" * @param indexPatternId used as layer's indexPattern to query for data + * @param layerDetails layer-specific field details + * */ -export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: string) => ({ +export const getDestinationLayer = ( + indexPatternTitle: string, + indexPatternId: string, + layerDetails: LayerMappingDetails +) => ({ sourceDescriptor: { id: uuid.v4(), type: 'ES_SEARCH', applyGlobalQuery: true, - geoField: 'destination.geo.location', + geoField: layerDetails.geoField, filterByMapBounds: true, - tooltipProperties: Object.keys(destinationFieldMappings), + tooltipProperties: layerDetails.tooltipProperties, useTopHits: false, topHitsTimeField: '@timestamp', topHitsSize: 1, @@ -162,7 +229,7 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s }, }, id: uuid.v4(), - label: `${indexPatternTitle} | ${i18n.DESTINATION_LAYER}`, + label: `${indexPatternTitle} | ${layerDetails.label}`, minZoom: 0, maxZoom: 24, alpha: 1, @@ -177,18 +244,31 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s * * @param indexPatternTitle used as layer name in LayerToC UI: "${indexPatternTitle} | Line" * @param indexPatternId used as layer's indexPattern to query for data + * @param layerDetails layer-specific field details */ -export const getLineLayer = (indexPatternTitle: string, indexPatternId: string) => ({ +export const getLineLayer = ( + indexPatternTitle: string, + indexPatternId: string, + layerDetails: LayerMapping +) => ({ sourceDescriptor: { type: 'ES_PEW_PEW', applyGlobalQuery: true, id: uuid.v4(), indexPatternId, - sourceGeoField: 'source.geo.location', - destGeoField: 'destination.geo.location', + sourceGeoField: layerDetails.source.geoField, + destGeoField: layerDetails.destination.geoField, metrics: [ - { type: 'sum', field: 'source.bytes', label: 'source.bytes' }, - { type: 'sum', field: 'destination.bytes', label: 'destination.bytes' }, + { + type: 'sum', + field: layerDetails.source.metricField, + label: layerDetails.source.metricField, + }, + { + type: 'sum', + field: layerDetails.destination.metricField, + label: layerDetails.destination.metricField, + }, ], }, style: { diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap index 05f507ec3a775b..bf62efe6a8263f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap @@ -49,3 +49,53 @@ exports[`LineToolTipContent renders correctly against snapshot 1`] = ` `; + +exports[`LineToolTipContent renders correctly against snapshot when rendering client & server 1`] = ` + + + + + + Client + + + + + + + + + + Server + + + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx index 824c717427763c..a0e57a2e850c19 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx @@ -8,7 +8,12 @@ import { shallow } from 'enzyme'; import React from 'react'; import { LineToolTipContentComponent } from './line_tool_tip_content'; import { FeatureProperty } from '../types'; -import { SUM_OF_DESTINATION_BYTES, SUM_OF_SOURCE_BYTES } from '../map_config'; +import { + SUM_OF_CLIENT_BYTES, + SUM_OF_DESTINATION_BYTES, + SUM_OF_SERVER_BYTES, + SUM_OF_SOURCE_BYTES, +} from '../map_config'; describe('LineToolTipContent', () => { const mockFeatureProps: FeatureProperty[] = [ @@ -22,10 +27,31 @@ describe('LineToolTipContent', () => { }, ]; + const mockClientServerFeatureProps: FeatureProperty[] = [ + { + _propertyKey: SUM_OF_SERVER_BYTES, + _rawValue: 'testPropValue', + }, + { + _propertyKey: SUM_OF_CLIENT_BYTES, + _rawValue: 'testPropValue', + }, + ]; + test('renders correctly against snapshot', () => { const wrapper = shallow( ); expect(wrapper).toMatchSnapshot(); }); + + test('renders correctly against snapshot when rendering client & server', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx index 0c416868bfb031..eff47699447651 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -8,7 +8,12 @@ import React from 'react'; import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { SourceDestinationArrows } from '../../source_destination/source_destination_arrows'; -import { SUM_OF_DESTINATION_BYTES, SUM_OF_SOURCE_BYTES } from '../map_config'; +import { + SUM_OF_CLIENT_BYTES, + SUM_OF_DESTINATION_BYTES, + SUM_OF_SERVER_BYTES, + SUM_OF_SOURCE_BYTES, +} from '../map_config'; import { FeatureProperty } from '../types'; import * as i18n from '../translations'; @@ -38,25 +43,29 @@ export const LineToolTipContentComponent = ({ {} ); + const isSrcDest = Object.keys(lineProps).includes(SUM_OF_SOURCE_BYTES); + return ( - {i18n.SOURCE} + {isSrcDest ? i18n.SOURCE : i18n.CLIENT} - {i18n.DESTINATION} + {isSrcDest ? i18n.DESTINATION : i18n.SERVER} diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts index 958619bee19d35..1e99a7219d4279 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts @@ -41,6 +41,20 @@ export const DESTINATION_LAYER = i18n.translate( } ); +export const CLIENT_LAYER = i18n.translate( + 'xpack.siem.components.embeddables.embeddedMap.clientLayerLabel', + { + defaultMessage: 'Client Point', + } +); + +export const SERVER_LAYER = i18n.translate( + 'xpack.siem.components.embeddables.embeddedMap.serverLayerLabel', + { + defaultMessage: 'Server Point', + } +); + export const LINE_LAYER = i18n.translate( 'xpack.siem.components.embeddables.embeddedMap.lineLayerLabel', { @@ -118,6 +132,20 @@ export const DESTINATION_IP = i18n.translate( } ); +export const CLIENT_IP = i18n.translate( + 'xpack.siem.components.embeddables.mapToolTip.pointContent.clientIPTitle', + { + defaultMessage: 'Client IP', + } +); + +export const SERVER_IP = i18n.translate( + 'xpack.siem.components.embeddables.mapToolTip.pointContent.serverIPTitle', + { + defaultMessage: 'Server IP', + } +); + export const SOURCE_DOMAIN = i18n.translate( 'xpack.siem.components.embeddables.mapToolTip.pointContent.sourceDomainTitle', { @@ -132,6 +160,20 @@ export const DESTINATION_DOMAIN = i18n.translate( } ); +export const CLIENT_DOMAIN = i18n.translate( + 'xpack.siem.components.embeddables.mapToolTip.pointContent.clientDomainTitle', + { + defaultMessage: 'Client domain', + } +); + +export const SERVER_DOMAIN = i18n.translate( + 'xpack.siem.components.embeddables.mapToolTip.pointContent.serverDomainTitle', + { + defaultMessage: 'Server domain', + } +); + export const LOCATION = i18n.translate( 'xpack.siem.components.embeddables.mapToolTip.pointContent.locationTitle', { @@ -159,3 +201,17 @@ export const DESTINATION = i18n.translate( defaultMessage: 'Destination', } ); + +export const CLIENT = i18n.translate( + 'xpack.siem.components.embeddables.mapToolTip.lineContent.clientLabel', + { + defaultMessage: 'Client', + } +); + +export const SERVER = i18n.translate( + 'xpack.siem.components.embeddables.mapToolTip.lineContent.serverLabel', + { + defaultMessage: 'Server', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts index fdda9f949280a8..6715a83e1b5090 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts @@ -31,6 +31,22 @@ export interface IndexPatternMapping { id: string; } +export interface LayerMappingDetails { + metricField: string; + geoField: string; + tooltipProperties: string[]; + label: string; +} + +export interface LayerMapping { + source: LayerMappingDetails; + destination: LayerMappingDetails; +} + +export interface LayerMappingCollection { + [indexPatternTitle: string]: LayerMapping; +} + export type SetQuery = (params: { id: string; inspect: inputsModel.InspectQuery | null; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index 4cf7cbb43cdc7f..33ed6a8c87b5fd 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -373,6 +373,7 @@ exports[`EventDetails rendering should match snapshot 1`] = ` "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", @@ -1064,6 +1065,7 @@ In other use cases the message field can be used to concatenate different values "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index 77d51f68c18ca4..05a33eeba14349 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -42,6 +42,10 @@ const WrappedByAutoSizer = styled.div` `; // required by AutoSizer WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +const StyledEuiPanel = styled(EuiPanel)` + max-width: 100%; +`; + interface Props { browserFields: BrowserFields; columns: ColumnHeader[]; @@ -113,7 +117,7 @@ const EventsViewerComponent: React.FC = ({ ); return ( - + {({ measureRef, content: { width = 0 } }) => ( <> @@ -225,7 +229,7 @@ const EventsViewerComponent: React.FC = ({ )} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx index db6ff7cf55f920..5a286532fabfca 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx @@ -55,7 +55,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine display="condensed" navTabs={ hideDetectionEngine - ? pickBy((_, key) => key !== SiemPageName.detectionEngine, navTabs) + ? pickBy((_, key) => key !== SiemPageName.detections, navTabs) : navTabs } /> diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 7c15af3fe642ad..3180fc955c6906 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,6 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; +import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { match: RouteMatch<{}>; @@ -60,26 +61,32 @@ export const LinkToPage = React.memo(({ match }) => ( + ; -export const DETECTION_ENGINE_PAGE_NAME = 'detection-engine'; +export const DETECTION_ENGINE_PAGE_NAME = 'detections'; export const RedirectToDetectionEnginePage = ({ + match: { + params: { tabName }, + }, location: { search }, -}: DetectionEngineComponentProps) => ( - -); +}: DetectionEngineComponentProps) => { + const defaultSelectedTab = DetectionEngineTab.signals; + const selectedTab = tabName ? tabName : defaultSelectedTab; + const to = `/${DETECTION_ENGINE_PAGE_NAME}/${selectedTab}${search}`; + + return ; +}; export const RedirectToRulesPage = ({ location: { search } }: DetectionEngineComponentProps) => { return ; @@ -28,7 +37,7 @@ export const RedirectToRulesPage = ({ location: { search } }: DetectionEngineCom export const RedirectToCreateRulePage = ({ location: { search }, }: DetectionEngineComponentProps) => { - return ; + return ; }; export const RedirectToRuleDetailsPage = ({ @@ -43,9 +52,15 @@ export const RedirectToEditRulePage = ({ location: { search } }: DetectionEngine ); }; -export const getDetectionEngineUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; -export const getRulesUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules`; -export const getCreateRuleUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/create-rule`; -export const getRuleDetailsUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details`; -export const getEditRuleUrl = () => - `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details/edit-rule`; +const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; + +export const getDetectionEngineUrl = () => `${baseDetectionEngineUrl}`; +export const getDetectionEngineAlertUrl = () => + `${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}`; +export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; +export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; +export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; +export const getRuleDetailsUrl = (detailName: string) => + `${baseDetectionEngineUrl}/rules/id/${detailName}`; +export const getEditRuleUrl = (detailName: string) => + `${baseDetectionEngineUrl}/rules/id/${detailName}/edit`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 56ebbb06f3eb98..cdd62c430a50cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -46,12 +46,12 @@ export const MatrixHistogramComponent: React.FC spyState != null && spyState.pageName === SiemPageName.hosts; +const isDetectionsRoutes = (spyState: RouteSpyState) => + spyState != null && spyState.pageName === SiemPageName.detections; + export const getBreadcrumbsForRoute = ( object: RouteSpyState & TabNavigationProps ): Breadcrumb[] | null => { @@ -76,6 +80,24 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isDetectionsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getDetectionRulesBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } if ( spyState != null && object.navTabs && diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index cae209a76fc1ce..56be39f67b1bdd 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -64,12 +64,12 @@ describe('SIEM Navigation', () => { expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, { detailName: undefined, navTabs: { - 'detection-engine': { + detections: { disabled: false, - href: '#/link-to/detection-engine', - id: 'detection-engine', - name: 'Detection engine', - urlKey: 'detection-engine', + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', }, hosts: { disabled: false, @@ -146,12 +146,12 @@ describe('SIEM Navigation', () => { detailName: undefined, filters: [], navTabs: { - 'detection-engine': { + detections: { disabled: false, - href: '#/link-to/detection-engine', - id: 'detection-engine', - name: 'Detection engine', - urlKey: 'detection-engine', + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', }, hosts: { disabled: false, @@ -187,6 +187,7 @@ describe('SIEM Navigation', () => { query: { language: 'kuery', query: '' }, savedQuery: undefined, search: '', + state: undefined, tabName: 'authentications', timeline: { id: '', isOpen: false }, timerange: { diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index 61ac84667d80f7..040a6e7847b772 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; +import isEqual from 'lodash/fp/isEqual'; +import deepEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; @@ -16,67 +17,78 @@ import { setBreadcrumbs } from './breadcrumbs'; import { TabNavigation } from './tab_navigation'; import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; -export const SiemNavigationComponent = React.memo< - SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState ->( - ({ detailName, display, navTabs, pageName, pathName, search, tabName, urlState, flowTarget }) => { - useEffect(() => { - if (pathName) { - setBreadcrumbs({ - query: urlState.query, - detailName, - filters: urlState.filters, - navTabs, - pageName, - pathName, - savedQuery: urlState.savedQuery, - search, - tabName, - flowTarget, - timerange: urlState.timerange, - timeline: urlState.timeline, - }); - } - }, [pathName, search, navTabs, urlState]); +export const SiemNavigationComponent: React.FC = ({ + detailName, + display, + navTabs, + pageName, + pathName, + search, + tabName, + urlState, + flowTarget, + state, +}) => { + useEffect(() => { + if (pathName) { + setBreadcrumbs({ + query: urlState.query, + detailName, + filters: urlState.filters, + navTabs, + pageName, + pathName, + savedQuery: urlState.savedQuery, + search, + tabName, + flowTarget, + timerange: urlState.timerange, + timeline: urlState.timeline, + state, + }); + } + }, [pathName, search, navTabs, urlState, state]); - return ( - - ); - }, - (prevProps, nextProps) => { - return ( + return ( + + ); +}; + +export const SiemNavigationRedux = compose< + React.ComponentClass +>(connect(makeMapStateToProps))( + React.memo( + SiemNavigationComponent, + (prevProps, nextProps) => prevProps.pathName === nextProps.pathName && prevProps.search === nextProps.search && isEqual(prevProps.navTabs, nextProps.navTabs) && - isEqual(prevProps.urlState, nextProps.urlState) - ); - } + isEqual(prevProps.urlState, nextProps.urlState) && + deepEqual(prevProps.state, nextProps.state) + ) ); -SiemNavigationComponent.displayName = 'SiemNavigationComponent'; - -export const SiemNavigationRedux = compose< - React.ComponentClass ->(connect(makeMapStateToProps))(SiemNavigationComponent); - -export const SiemNavigation = React.memo(props => { +const SiemNavigationContainer: React.FC = props => { const [routeProps] = useRouteSpy(); const stateNavReduxProps: RouteSpyState & SiemNavigationProps = { ...routeProps, ...props, }; + return ; -}); +}; -SiemNavigation.displayName = 'SiemNavigation'; +export const SiemNavigation = SiemNavigationContainer; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index dfea99ffd70910..b8b03be4e47208 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -378,6 +378,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index a0122093e75040..3608a81234677c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -370,6 +370,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index 6fc9145187a365..0a60c8facff9c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -365,6 +365,7 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index b0431684050cf7..9b59f69cad3a33 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -370,6 +370,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` "example": null, "format": "", "indexes": Array [ + "apm-*-transaction*", "auditbeat-*", "endgame-*", "filebeat-*", diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx index c7259edbdc5937..009ab141e958e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -45,11 +45,25 @@ const MyEuiFlexItem = styled(EuiFlexItem)` white-space: nowrap; `; -const EuiSelectableContainer = styled.div` +const EuiSelectableContainer = styled.div<{ loading: boolean }>` .euiSelectable { .euiFormControlLayout__childrenWrapper { display: flex; } + ${({ loading }) => `${ + loading + ? ` + .euiFormControlLayoutIcons { + display: none; + } + .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { + display: block; + left: 12px; + top: 12px; + }` + : '' + } + `} } `; @@ -265,7 +279,7 @@ const SearchTimelineSuperSelectComponent: React.FC {({ timelines, loading, totalCount }) => ( - + { - describe('isKqlForRoute', () => { - test('host page and host page kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsPage); - expect(result).toBeTruthy(); - }); - test('host page and host details kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsDetails); - expect(result).toBeFalsy(); - }); - test('host details and host details kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsDetails); - expect(result).toBeTruthy(); - }); - test('host details and host page kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsPage); - expect(result).toBeFalsy(); - }); - test('network page and network page kuery', () => { - const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkPage); - expect(result).toBeTruthy(); - }); - test('network page and network details kuery', () => { - const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkDetails); - expect(result).toBeFalsy(); - }); - test('network details and network details kuery', () => { - const result = isKqlForRoute(SiemPageName.network, '10.100.7.198', CONSTANTS.networkDetails); - expect(result).toBeTruthy(); - }); - test('network details and network page kuery', () => { - const result = isKqlForRoute(SiemPageName.network, '123.234.34', CONSTANTS.networkPage); - expect(result).toBeFalsy(); - }); - }); describe('getTitle', () => { test('host page name', () => { const result = getTitle('hosts', undefined, navTabs); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index aa340b54c1699f..6ba5810f794b09 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -78,8 +78,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'host'; } else if (pageName === SiemPageName.network) { return 'network'; - } else if (pageName === SiemPageName.detectionEngine) { - return 'detection-engine'; + } else if (pageName === SiemPageName.detections) { + return 'detections'; } else if (pageName === SiemPageName.timelines) { return 'timeline'; } @@ -111,31 +111,14 @@ export const getCurrentLocation = ( return CONSTANTS.networkDetails; } return CONSTANTS.networkPage; - } else if (pageName === SiemPageName.detectionEngine) { - return CONSTANTS.detectionEnginePage; + } else if (pageName === SiemPageName.detections) { + return CONSTANTS.detectionsPage; } else if (pageName === SiemPageName.timelines) { return CONSTANTS.timelinePage; } return CONSTANTS.unknown; }; -export const isKqlForRoute = ( - pageName: string, - detailName: string | undefined, - queryLocation: LocationTypes | null = null -): boolean => { - const currentLocation = getCurrentLocation(pageName, detailName); - if ( - (currentLocation === CONSTANTS.hostsPage && queryLocation === CONSTANTS.hostsPage) || - (currentLocation === CONSTANTS.networkPage && queryLocation === CONSTANTS.networkPage) || - (currentLocation === CONSTANTS.hostsDetails && queryLocation === CONSTANTS.hostsDetails) || - (currentLocation === CONSTANTS.networkDetails && queryLocation === CONSTANTS.networkDetails) - ) { - return true; - } - return false; -}; - export const makeMapStateToProps = () => { const getInputsSelector = inputsSelectors.inputsSelector(); const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index a48653a7ea6f4a..be1ae1ad63bd4b 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -24,7 +24,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ ]; export const URL_STATE_KEYS: Record = { - 'detection-engine': [ + detections: [ CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, @@ -56,7 +56,7 @@ export const URL_STATE_KEYS: Record = { }; export type LocationTypes = - | CONSTANTS.detectionEnginePage + | CONSTANTS.detectionsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage | CONSTANTS.networkDetails diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx index f691219e446a8f..2d9ac8b7645ca3 100644 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -39,6 +39,14 @@ export const AnomaliesQueryTabBody = ({ flowTarget, ip, }: AnomaliesQueryTabBodyProps) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + const [, siemJobs] = useSiemJobs(true); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); @@ -51,21 +59,12 @@ export const AnomaliesQueryTabBody = ({ ip ); - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - return ( <> => { - const requests = rules.map(rule => - fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { + const response = await fetch( + `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', 'kbn-xsrf': 'true', }, - body: JSON.stringify({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: rule.enabled, - immutable: false, - last_success_at: undefined, - last_success_message: undefined, - status: undefined, - status_date: undefined, - }), - }) + body: JSON.stringify( + rules.map(rule => ({ + ...rule, + name: `${rule.name} [${i18n.DUPLICATE}]`, + created_at: undefined, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_at: undefined, + updated_by: undefined, + enabled: rule.enabled, + immutable: undefined, + last_success_at: undefined, + last_success_message: undefined, + status: undefined, + status_date: undefined, + })) + ), + } ); - const responses = await Promise.all(requests); - await responses.map(response => throwIfNotOk(response)); - return Promise.all( - responses.map>(response => response.json()) - ); + await throwIfNotOk(response); + return response.json(); }; /** @@ -322,7 +322,7 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise> => { +}): Promise> => { const response = await fetch( `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent( JSON.stringify([id]) diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts index a61cbabd80626e..e9a0f27b346960 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts @@ -10,3 +10,4 @@ export * from './persist_rule'; export * from './types'; export * from './use_rule'; export * from './use_rules'; +export * from './use_rule_status'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index feef888c0d47ff..0dcd0da5be8f61 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -146,7 +146,7 @@ export interface DeleteRulesProps { } export interface DuplicateRulesProps { - rules: Rules; + rules: Rule[]; } export interface BasicFetchProps { @@ -181,9 +181,15 @@ export interface ExportRulesProps { } export interface RuleStatus { + current_status: RuleInfoStatus; + failures: RuleInfoStatus[]; +} + +export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export interface RuleInfoStatus { alert_id: string; status_date: string; - status: string; + status: RuleStatusType | null; last_failure_at: string | null; last_success_at: string | null; last_failure_message: string | null; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx new file mode 100644 index 00000000000000..592419f8790113 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { createPrepackagedRules } from './api'; + +type Return = [boolean, boolean | null]; + +interface UseCreatePackagedRules { + canUserCRUD: boolean | null; + hasIndexManage: boolean | null; + hasManageApiKey: boolean | null; + isAuthenticated: boolean | null; + isSignalIndexExists: boolean | null; +} + +/** + * Hook for creating the packages rules + * + * @param canUserCRUD boolean + * @param hasIndexManage boolean + * @param hasManageApiKey boolean + * @param isAuthenticated boolean + * @param isSignalIndexExists boolean + * + * @returns [loading, hasCreatedPackageRules] + */ +export const useCreatePackagedRules = ({ + canUserCRUD, + hasIndexManage, + hasManageApiKey, + isAuthenticated, + isSignalIndexExists, +}: UseCreatePackagedRules): Return => { + const [hasCreatedPackageRules, setHasCreatedPackageRules] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function createRules() { + try { + await createPrepackagedRules({ + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setHasCreatedPackageRules(true); + } + } catch (error) { + if (isSubscribed) { + setHasCreatedPackageRules(false); + } + } + if (isSubscribed) { + setLoading(false); + } + } + if ( + canUserCRUD && + hasIndexManage && + hasManageApiKey && + isAuthenticated && + isSignalIndexExists + ) { + createRules(); + } + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]); + + return [loading, hasCreatedPackageRules]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index 216fbcea861a3c..466c2cddac97d1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; @@ -12,7 +12,8 @@ import { getRuleStatusById } from './api'; import * as i18n from './translations'; import { RuleStatus } from './types'; -type Return = [boolean, RuleStatus[] | null]; +type Func = (ruleId: string) => void; +type Return = [boolean, RuleStatus | null, Func | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -21,7 +22,8 @@ type Return = [boolean, RuleStatus[] | null]; * */ export const useRuleStatus = (id: string | undefined | null): Return => { - const [ruleStatus, setRuleStatus] = useState(null); + const [ruleStatus, setRuleStatus] = useState(null); + const fetchRuleStatus = useRef(null); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -29,7 +31,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => { let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData(idToFetch: string) { + const fetchData = async (idToFetch: string) => { try { setLoading(true); const ruleStatusResponse = await getRuleStatusById({ @@ -49,15 +51,16 @@ export const useRuleStatus = (id: string | undefined | null): Return => { if (isSubscribed) { setLoading(false); } - } + }; if (id != null) { fetchData(id); } + fetchRuleStatus.current = fetchData; return () => { isSubscribed = false; abortCtrl.abort(); }; }, [id]); - return [loading, ruleStatus]; + return [loading, ruleStatus, fetchRuleStatus.current]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 34cb7684a03993..ea4860dafd40f2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -96,5 +96,5 @@ export interface Privilege { write: boolean; }; }; - isAuthenticated: boolean; + is_authenticated: boolean; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index 792ff29ad24885..7d0e331200d55e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -42,7 +42,7 @@ export const usePrivilegeUser = (): Return => { }); if (isSubscribed && privilege != null) { - setAuthenticated(privilege.isAuthenticated); + setAuthenticated(privilege.is_authenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; setHasIndexManage(privilege.index[indexName].manage); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx index 189d8a1bf3f758..c1ee5fd12b8c11 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx @@ -8,7 +8,6 @@ import { useEffect, useState, useRef } from 'react'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../../components/toasters'; -import { createPrepackagedRules } from '../rules'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { PostSignalError, SignalIndexError } from './types'; @@ -41,7 +40,6 @@ export const useSignalIndex = (): Return => { if (isSubscribed && signal != null) { setSignalIndexName(signal.name); setSignalIndexExists(true); - createPrepackagedRules({ signal: abortCtrl.signal }); } } catch (error) { if (isSubscribed) { diff --git a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx index 6361f7abcf9770..4eb51dfe6407c7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx @@ -5,10 +5,7 @@ */ import React, { useState } from 'react'; -import { - AutocompleteSuggestion, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +import { autocomplete, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { useKibana } from '../../lib/kibana'; type RendererResult = React.ReactElement | null; @@ -18,7 +15,7 @@ interface KueryAutocompletionLifecycleProps { children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; }>; indexPattern: IIndexPattern; } @@ -33,26 +30,19 @@ export const KueryAutocompletion = React.memo const [currentRequest, setCurrentRequest] = useState( null ); - const [suggestions, setSuggestions] = useState([]); + const [suggestions, setSuggestions] = useState([]); const kibana = useKibana(); const loadSuggestions = async ( expression: string, cursorPosition: number, maxSuggestions?: number ) => { - const autocompletionProvider = kibana.services.data.autocomplete.getProvider('kuery'); - const config = { - get: () => true, - }; - if (!autocompletionProvider) { + const language = 'kuery'; + + if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { return; } - const getSuggestions = autocompletionProvider({ - config, - indexPatterns: [indexPattern], - boolFilter: [], - }); const futureRequest = { expression, cursorPosition, @@ -62,16 +52,22 @@ export const KueryAutocompletion = React.memo cursorPosition, }); setSuggestions([]); - const newSuggestions = await getSuggestions({ - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - }); + if ( futureRequest && futureRequest.expression !== (currentRequest && currentRequest.expression) && futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) ) { + const newSuggestions = + (await kibana.services.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + })) || []; + setCurrentRequest(null); setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); } diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx index 5b1be4ca2c1dca..d5fd325bb9a26e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx @@ -26,7 +26,6 @@ import { SetQuery } from '../../pages/hosts/navigation/types'; export interface OwnProps extends QueryTemplateProps { dataKey: string | string[]; defaultStackByOption: MatrixHistogramOption; - deleteQuery?: ({ id }: { id: string }) => void; errorMessage: string; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts index 9cda9d8f6115f5..1df1aec76627ce 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { getOr } from 'lodash/fp'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { MatrixHistogramDataTypes, MatrixHistogramQueryProps, @@ -35,7 +35,7 @@ export const useQuery = ({ }: MatrixHistogramQueryProps) => { const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const [, dispatchToaster] = useStateToaster(); - const [refetch, setRefetch] = useState(); + const refetch = useRef(); const [loading, setLoading] = useState(false); const [data, setData] = useState(null); const [inspect, setInspect] = useState(null); @@ -71,7 +71,7 @@ export const useQuery = ({ return apolloClient .query({ query, - fetchPolicy: 'cache-first', + fetchPolicy: 'network-only', variables: matrixHistogramVariables, context: { fetchOptions: { @@ -103,9 +103,7 @@ export const useQuery = ({ } ); } - setRefetch(() => { - fetchData(); - }); + refetch.current = fetchData; fetchData(); return () => { isSubscribed = false; @@ -122,5 +120,5 @@ export const useQuery = ({ endDate, ]); - return { data, loading, inspect, totalCount, refetch }; + return { data, loading, inspect, totalCount, refetch: refetch.current }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index c585e04d2cfd7b..97f007452854ca 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -81,8 +81,7 @@ class TimelineQueryComponent extends QueryTemplate< sourceId, sortField, } = this.props; - // I needed to do that to avoid test to yell at me since there is no good way yet to mock withKibana - const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY) ?? []; + const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); const defaultIndex = isEmpty(indexPattern) ? [...defaultKibanaIndex, ...indexToAdd] : indexPattern?.title.split(',') ?? []; diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts index 7d843977d1f322..968ab6543f4fc5 100644 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts +++ b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts @@ -71,7 +71,18 @@ export const createUseUiSetting$Mock = () => { }; export const createUseKibanaMock = () => { - const services = { ...createKibanaCoreStartMock(), ...createKibanaPluginsStartMock() }; + const core = createKibanaCoreStartMock(); + const plugins = createKibanaPluginsStartMock(); + const useUiSetting = createUseUiSettingMock(); + + const services = { + ...core, + ...plugins, + uiSettings: { + ...core.uiSettings, + get: useUiSetting, + }, + }; return () => ({ services }); }; @@ -87,15 +98,11 @@ export const createWithKibanaMock = () => { export const createKibanaContextProviderMock = () => { const kibana = createUseKibanaMock()(); - const uiSettings = { - ...kibana.services.uiSettings, - get: createUseUiSettingMock(), - }; // eslint-disable-next-line @typescript-eslint/no-explicit-any return ({ services, ...rest }: any) => React.createElement(KibanaContextProvider, { ...rest, - services: { ...kibana.services, uiSettings, ...services }, + services: { ...kibana.services, ...services }, }); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx index 8290da1ba3220a..5f017a3a1f67fd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx @@ -45,7 +45,7 @@ export const ActivityMonitor = React.memo(() => { { id: 1, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -55,7 +55,7 @@ export const ActivityMonitor = React.memo(() => { { id: 2, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -65,7 +65,7 @@ export const ActivityMonitor = React.memo(() => { { id: 3, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -76,7 +76,7 @@ export const ActivityMonitor = React.memo(() => { { id: 4, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -87,7 +87,7 @@ export const ActivityMonitor = React.memo(() => { { id: 5, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -98,7 +98,7 @@ export const ActivityMonitor = React.memo(() => { { id: 6, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -109,7 +109,7 @@ export const ActivityMonitor = React.memo(() => { { id: 7, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -120,7 +120,7 @@ export const ActivityMonitor = React.memo(() => { { id: 8, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -131,7 +131,7 @@ export const ActivityMonitor = React.memo(() => { { id: 9, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -142,7 +142,7 @@ export const ActivityMonitor = React.memo(() => { { id: 10, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -153,7 +153,7 @@ export const ActivityMonitor = React.memo(() => { { id: 11, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -164,7 +164,7 @@ export const ActivityMonitor = React.memo(() => { { id: 12, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -175,7 +175,7 @@ export const ActivityMonitor = React.memo(() => { { id: 13, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -186,7 +186,7 @@ export const ActivityMonitor = React.memo(() => { { id: 14, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -197,7 +197,7 @@ export const ActivityMonitor = React.memo(() => { { id: 15, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -208,7 +208,7 @@ export const ActivityMonitor = React.memo(() => { { id: 16, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -219,7 +219,7 @@ export const ActivityMonitor = React.memo(() => { { id: 17, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -230,7 +230,7 @@ export const ActivityMonitor = React.memo(() => { { id: 18, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -241,7 +241,7 @@ export const ActivityMonitor = React.memo(() => { { id: 19, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -252,7 +252,7 @@ export const ActivityMonitor = React.memo(() => { { id: 20, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -263,7 +263,7 @@ export const ActivityMonitor = React.memo(() => { { id: 21, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index 5c4795a8192753..e00dfa5b844739 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -188,13 +188,13 @@ export const getSignalsActions = ({ updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; }): TimelineAction[] => [ { - getAction: ({ eventId, ecsData }: TimelineActionProps): JSX.Element => ( + getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( sendSignalToTimelineAction({ apolloClient, @@ -203,7 +203,7 @@ export const getSignalsActions = ({ updateTimelineIsLoading, }) } - iconType="tableDensityNormal" + iconType="timeline" aria-label="Next" /> @@ -228,7 +228,7 @@ export const getSignalsActions = ({ }) } isDisabled={!canUserCRUD || !hasIndexWrite} - iconType={status === FILTER_OPEN ? 'indexOpen' : 'indexClose'} + iconType={status === FILTER_OPEN ? 'securitySignalDetected' : 'securitySignalResolved'} aria-label="Next" /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx index b756b2eb75a7a7..bb45ff68cb01d6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/batch_actions.tsx @@ -61,7 +61,7 @@ export const getBatchItems = ({ { closePopover(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 0af3635d4c473b..13d77385c53d42 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -80,7 +80,7 @@ const SignalsUtilityBarComponent: React.FC = ({ {isFilteredToOpen @@ -89,7 +89,7 @@ const SignalsUtilityBarComponent: React.FC = ({ { if (!showClearSelection) { selectAll(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts index d1ba946be41de1..c262f907c98763 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts @@ -11,7 +11,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', }); export const SIGNALS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.signals.tableTitle', { - defaultMessage: 'All signals', + defaultMessage: 'Signals', }); export const SIGNALS_DOCUMENT_TYPE = i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts index f329780b075e3f..d475fd155ea25d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as i18n from './translations'; import { SignalsHistogramOption } from './types'; export const signalsHistogramOptions: SignalsHistogramOption[] = [ - { text: i18n.STACK_BY_RISK_SCORES, value: 'signal.rule.risk_score' }, - { text: i18n.STACK_BY_SEVERITIES, value: 'signal.rule.severity' }, - { text: i18n.STACK_BY_DESTINATION_IPS, value: 'destination.ip' }, - { text: i18n.STACK_BY_ACTIONS, value: 'event.action' }, - { text: i18n.STACK_BY_CATEGORIES, value: 'event.category' }, - { text: i18n.STACK_BY_HOST_NAMES, value: 'host.name' }, - { text: i18n.STACK_BY_RULE_TYPES, value: 'signal.rule.type' }, - { text: i18n.STACK_BY_RULE_NAMES, value: 'signal.rule.name' }, - { text: i18n.STACK_BY_SOURCE_IPS, value: 'source.ip' }, - { text: i18n.STACK_BY_USERS, value: 'user.name' }, + { text: 'signal.rule.risk_score', value: 'signal.rule.risk_score' }, + { text: 'signal.rule.severity', value: 'signal.rule.severity' }, + { text: 'destination.ip', value: 'destination.ip' }, + { text: 'event.action', value: 'event.action' }, + { text: 'event.category', value: 'event.category' }, + { text: 'host.name', value: 'host.name' }, + { text: 'signal.rule.type', value: 'signal.rule.type' }, + { text: 'signal.rule.name', value: 'signal.rule.name' }, + { text: 'source.ip', value: 'source.ip' }, + { text: 'user.name', value: 'user.name' }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx index fda40f5f9fa5db..64bc7ba24c6895 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -46,7 +46,7 @@ export const SignalsHistogramPanel = memo( filters, query, from, - legendPosition = 'bottom', + legendPosition = 'right', loadingInitial = false, showLinkToSignals = false, showTotalSignalsCount = false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx index 218fcc3a70f79f..d4db8cc7c37e87 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx @@ -44,7 +44,7 @@ export const SignalsHistogram = React.memo( from, query, filters, - legendPosition = 'bottom', + legendPosition = 'right', loadingInitial, setTotalSignalsCount, stackByField, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts index 0245b9968cc360..8c88fa4a5dae60 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts @@ -86,7 +86,7 @@ export const STACK_BY_USERS = i18n.translate( export const HISTOGRAM_HEADER = i18n.translate( 'xpack.siem.detectionEngine.signals.histogram.headerTitle', { - defaultMessage: 'Signal detection frequency', + defaultMessage: 'Signal count', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index bbaccb7882484b..24e14473d40e98 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -10,6 +10,7 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; import { useKibana } from '../../../../lib/kibana'; +import { useCreatePackagedRules } from '../../../../containers/detection_engine/rules/use_create_packaged_rules'; export interface State { canUserCRUD: boolean | null; @@ -161,6 +162,14 @@ export const useUserInfo = (): State => { createSignalIndex, ] = useSignalIndex(); + useCreatePackagedRules({ + canUserCRUD, + hasIndexManage, + hasManageApiKey, + isAuthenticated, + isSignalIndexExists, + }); + const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 388f667f47fe12..5586749ce38d83 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,27 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import { EuiButton, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; - import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; + +import { Query } from '../../../../../../../src/plugins/data/common/query'; +import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; + +import { GlobalTime } from '../../containers/global_time'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; +import { AlertsTable } from '../../components/alerts_viewer/alerts_table'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; -import { GlobalTime } from '../../containers/global_time'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { SpyRoute } from '../../utils/route/spy_routes'; - -import { Query } from '../../../../../../../src/plugins/data/common/query'; -import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; import { State } from '../../store'; import { inputsSelectors } from '../../store/inputs'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { SpyRoute } from '../../utils/route/spy_routes'; import { InputsModelId } from '../../store/inputs/constants'; import { InputsRange } from '../../store/inputs/model'; +import { AlertsByCategory } from '../overview/alerts_by_category'; import { useSignalInfo } from './components/signals_info'; import { SignalsTable } from './components/signals'; import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; @@ -35,6 +39,7 @@ import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; +import { DetectionEngineTab } from './types'; interface ReduxProps { filters: esFilters.Filter[]; @@ -51,8 +56,22 @@ export interface DispatchProps { type DetectionEngineComponentProps = ReduxProps & DispatchProps; +const detectionsTabs = [ + { + id: DetectionEngineTab.signals, + name: i18n.SIGNAL, + disabled: false, + }, + { + id: DetectionEngineTab.alerts, + name: i18n.ALERT, + disabled: false, + }, +]; + const DetectionEngineComponent = React.memo( ({ filters, query, setAbsoluteRangeDatePicker }) => { + const { tabName = DetectionEngineTab.signals } = useParams(); const { loading, isSignalIndexExists, @@ -87,6 +106,25 @@ const DetectionEngineComponent = React.memo( ); } + + const tabs = useMemo( + () => ( + + {detectionsTabs.map(tab => ( + + {tab.name} + + ))} + + ), + [detectionsTabs, tabName] + ); + return ( <> {hasIndexWrite != null && !hasIndexWrite && } @@ -99,7 +137,6 @@ const DetectionEngineComponent = React.memo( @@ -111,32 +148,55 @@ const DetectionEngineComponent = React.memo( } title={i18n.PAGE_TITLE} > - + {i18n.BUTTON_MANAGE_RULES} - {({ to, from }) => ( + {({ to, from, deleteQuery, setQuery }) => ( <> - + {tabs} - + {tabName === DetectionEngineTab.signals && ( + <> + + + + + )} + {tabName === DetectionEngineTab.alerts && ( + <> + + + + + )} )} @@ -170,8 +230,13 @@ const makeMapStateToProps = () => { }; }; -export const DetectionEngine = connect(makeMapStateToProps, { +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -})(DetectionEngineComponent); +}; + +export const DetectionEngine = connect( + makeMapStateToProps, + mapDispatchToProps +)(DetectionEngineComponent); DetectionEngine.displayName = 'DetectionEngine'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index c4e83429aebdbf..6db8d93e46ac9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -7,21 +7,26 @@ import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { ManageUserInfo } from './components/user_info'; import { CreateRuleComponent } from './rules/create'; import { DetectionEngine } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; import { RuleDetails } from './rules/details'; import { RulesComponent } from './rules'; -import { ManageUserInfo } from './components/user_info'; +import { DetectionEngineTab } from './types'; -const detectionEnginePath = `/:pageName(detection-engine)`; +const detectionEnginePath = `/:pageName(detections)`; type Props = Partial> & { url: string }; -export const DetectionEngineContainer = React.memo(() => ( +const DetectionEngineContainerComponent: React.FC = () => ( - + @@ -30,19 +35,20 @@ export const DetectionEngineContainer = React.memo(() => ( - + - + ( - + )} /> -)); -DetectionEngineContainer.displayName = 'DetectionEngineContainer'; +); + +export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 757c1fabfc9cd6..b79b3ed091f169 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -60,7 +60,7 @@ export const mockTableData: TableData[] = [ lastResponse: { type: '—' }, method: 'saved_query', rule: { - href: '#/detection-engine/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61', + href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61', name: 'Home Grown!', status: 'Status Placeholder', }, @@ -112,7 +112,7 @@ export const mockTableData: TableData[] = [ lastResponse: { type: '—' }, method: 'saved_query', rule: { - href: '#/detection-engine/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee', + href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee', name: 'Home Grown!', status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index f83a19445acd6b..435edcab433b6c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -29,17 +29,25 @@ export const editRuleAction = (rule: Rule, history: H.History) => { history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); }; -export const duplicateRuleAction = async ( - rule: Rule, +export const duplicateRulesAction = async ( + rules: Rule[], dispatch: React.Dispatch, dispatchToaster: Dispatch ) => { try { - dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); - const duplicatedRule = await duplicateRules({ rules: [rule] }); - dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); - dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id }); - displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRule.length), dispatchToaster); + const ruleIds = rules.map(r => r.id); + dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true }); + const duplicatedRules = await duplicateRules({ rules }); + dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: false }); + dispatch({ + type: 'updateRules', + rules: duplicatedRules, + appendRuleId: rules[rules.length - 1].id, + }); + displaySuccessToast( + i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length), + dispatchToaster + ); } catch (e) { displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx index 06d4c709a32bfd..8a10d4f7100b94 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx @@ -10,9 +10,13 @@ import * as H from 'history'; import * as i18n from '../translations'; import { TableData } from '../types'; import { Action } from './reducer'; -import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions'; +import { + deleteRulesAction, + duplicateRulesAction, + enableRulesAction, + exportRulesAction, +} from './actions'; import { ActionToaster } from '../../../../components/toasters'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; export const getBatchItems = ( selectedState: TableData[], @@ -25,7 +29,6 @@ export const getBatchItems = ( const containsDisabled = selectedState.some(v => !v.activate); const containsLoading = selectedState.some(v => v.isLoading); const containsImmutable = selectedState.some(v => v.immutable); - const containsMultipleRules = Array.from(new Set(selectedState.map(v => v.rule_id))).length > 1; return [ , { closePopover(); - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${selectedState[0].id}/edit`); + await duplicateRulesAction( + selectedState.map(s => s.sourceRule), + dispatch, + dispatchToaster + ); }} > - {i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS} + {i18n.BATCH_ACTION_DUPLICATE_SELECTED} , { closePopover(); await deleteRulesAction( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 91b018eb3078f2..d546c4edb55d35 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -18,7 +18,7 @@ import React, { Dispatch } from 'react'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { deleteRulesAction, - duplicateRuleAction, + duplicateRulesAction, editRuleAction, exportRulesAction, } from './actions'; @@ -30,6 +30,7 @@ import { FormattedDate } from '../../../../components/formatted_date'; import { RuleSwitch } from '../components/rule_switch'; import { SeverityBadge } from '../components/severity_badge'; import { ActionToaster } from '../../../../components/toasters'; +import { getStatusColor } from '../components/rule_status/helpers'; const getActions = ( dispatch: React.Dispatch, @@ -48,7 +49,7 @@ const getActions = ( icon: 'copy', name: i18n.DUPLICATE_RULE, onClick: (rowItem: TableData) => - duplicateRuleAction(rowItem.sourceRule, dispatch, dispatchToaster), + duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster), }, { description: i18n.EXPORT_RULE, @@ -62,7 +63,6 @@ const getActions = ( icon: 'trash', name: i18n.DELETE_RULE, onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster), - enabled: (rowItem: TableData) => !rowItem.immutable, }, ]; @@ -87,7 +87,7 @@ export const getColumns = ( field: 'method', name: i18n.COLUMN_METHOD, truncateText: true, - width: '16%', + width: '14%', }, { field: 'severity', @@ -114,19 +114,11 @@ export const getColumns = ( field: 'status', name: i18n.COLUMN_LAST_RESPONSE, render: (value: TableData['status']) => { - const color = - value == null - ? 'subdued' - : value === 'succeeded' - ? 'success' - : value === 'failed' - ? 'danger' - : value === 'executing' - ? 'warning' - : 'subdued'; return ( <> - {value ?? getEmptyTagValue()} + + {value ?? getEmptyTagValue()} + ); }, @@ -162,7 +154,7 @@ export const getColumns = ( /> ), sortable: true, - width: '85px', + width: '95px', }, ]; const actions: RulesColumns[] = [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 9666b7a5688cf7..07a2f2f2789874 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -24,7 +24,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] immutable: rule.immutable, rule_id: rule.rule_id, rule: { - href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`, + href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`, name: rule.name, status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index e8b6919165c8bf..011c008c5b2d2b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -125,7 +125,7 @@ export const buildThreatsDescription = ({ description: ( {threats.map((threat, index) => { - const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); + const tactic = tacticsOptions.find(t => t.id === threat.tactic.id); return ( @@ -133,7 +133,7 @@ export const buildThreatsDescription = ({ {threat.techniques.map(technique => { - const myTechnique = techniquesOptions.find(t => t.name === technique.name); + const myTechnique = techniquesOptions.find(t => t.id === technique.id); return ( - theme.euiColorPrimary}; + width: 40px; + height: 40px; + } +`; + interface RuleActionsOverflowComponentProps { rule: Rule | null; userHasNoPermissions: boolean; @@ -54,7 +66,7 @@ const RuleActionsOverflowComponent = ({ disabled={userHasNoPermissions} onClick={async () => { setIsPopoverOpen(false); - await duplicateRuleAction(rule, noop, dispatchToaster); + await duplicateRulesAction([rule], noop, dispatchToaster); }} > {i18nActions.DUPLICATE_RULE} @@ -73,7 +85,7 @@ const RuleActionsOverflowComponent = ({ { setIsPopoverOpen(false); await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); @@ -86,20 +98,29 @@ const RuleActionsOverflowComponent = ({ [rule, userHasNoPermissions] ); + const handlePopoverOpen = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [setIsPopoverOpen, isPopoverOpen]); + + const button = useMemo( + () => ( + + + + ), + [handlePopoverOpen, userHasNoPermissions] + ); + return ( <> - setIsPopoverOpen(!isPopoverOpen)} - /> - - } + button={button} closePopover={() => setIsPopoverOpen(false)} id="ruleActionsOverflow" isOpen={isPopoverOpen} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts new file mode 100644 index 00000000000000..263f602251ea77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.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 { RuleStatusType } from '../../../../../containers/detection_engine/rules'; + +export const getStatusColor = (status: RuleStatusType | string | null) => + status == null + ? 'subdued' + : status === 'succeeded' + ? 'success' + : status === 'failed' + ? 'danger' + : status === 'executing' || status === 'going to run' + ? 'warning' + : 'subdued'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx new file mode 100644 index 00000000000000..2c9173cbeb694a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules'; +import { FormattedDate } from '../../../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../../../components/empty_value'; +import { getStatusColor } from './helpers'; +import * as i18n from './translations'; + +interface RuleStatusProps { + ruleId: string | null; + ruleEnabled?: boolean | null; +} + +const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) => { + const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId); + const [myRuleEnabled, setMyRuleEnabled] = useState(ruleEnabled ?? null); + const [currentStatus, setCurrentStatus] = useState( + ruleStatus?.current_status ?? null + ); + + useEffect(() => { + if (myRuleEnabled !== ruleEnabled && fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + if (myRuleEnabled !== ruleEnabled) { + setMyRuleEnabled(ruleEnabled ?? null); + } + } + }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); + + useEffect(() => { + if (!isEqual(currentStatus, ruleStatus?.current_status)) { + setCurrentStatus(ruleStatus?.current_status ?? null); + } + }, [currentStatus, ruleStatus, setCurrentStatus]); + + const handleRefresh = useCallback(() => { + if (fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + } + }, [fetchRuleStatus, ruleId]); + + return ( + + + {i18n.STATUS} + {':'} + + {loading && ( + + + + )} + {!loading && ( + <> + + + {currentStatus?.status ?? getEmptyTagValue()} + + + {currentStatus?.status_date != null && currentStatus?.status != null && ( + <> + + <>{i18n.STATUS_AT} + + + + + + )} + + + + + )} + + ); +}; + +export const RuleStatus = memo(RuleStatusComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts new file mode 100644 index 00000000000000..e03cc252ad729f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleStatus.statusDescription', { + defaultMessage: 'Status', +}); + +export const STATUS_AT = i18n.translate( + 'xpack.siem.detectionEngine.ruleStatus.statusAtDescription', + { + defaultMessage: 'at', + } +); + +export const STATUS_DATE = i18n.translate( + 'xpack.siem.detectionEngine.ruleStatus.statusDateDescription', + { + defaultMessage: 'Status date', + } +); + +export const REFRESH = i18n.translate('xpack.siem.detectionEngine.ruleStatus.refreshButton', { + defaultMessage: 'Refresh', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index 9cb0323ed8987a..09b7ecc9df9823 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -36,6 +36,7 @@ export interface RuleSwitchProps { isDisabled?: boolean; isLoading?: boolean; optionLabel?: string; + onChange?: (enabled: boolean) => void; } /** @@ -48,6 +49,7 @@ export const RuleSwitchComponent = ({ isLoading, enabled, optionLabel, + onChange, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); const [myEnabled, setMyEnabled] = useState(enabled ?? false); @@ -65,6 +67,9 @@ export const RuleSwitchComponent = ({ enabled: event.target.checked!, }); setMyEnabled(updatedRules[0].enabled); + if (onChange != null) { + onChange(updatedRules[0].enabled); + } } catch { setMyIsLoading(false); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 0ef104e6891df0..3bde2087f26b1c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -150,7 +150,13 @@ export const ScheduleItem = ({ /> } > - + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index 328c4a0f960667..92aca1cecf9f30 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -5,6 +5,7 @@ */ import { AboutStepRule } from '../../types'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations'; export const threatsDefault = [ { @@ -25,7 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = { tags: [], timeline: { id: null, - title: null, + title: DEFAULT_TIMELINE_TITLE, }, threats: threatsDefault, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 0e03a11776fb7e..73c07673a82f44 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -5,10 +5,11 @@ */ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEqual, get } from 'lodash/fp'; +import { isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { setFieldValue } from '../../helpers'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; import * as RuleI18n from '../../translations'; import { AddItem } from '../add_item_form'; @@ -71,14 +72,7 @@ const StepAboutRuleComponent: FC = ({ isNew: false, }; setMyStepData(myDefaultValues); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + setFieldValue(form, schema, myDefaultValues); } }, [defaultValues]); @@ -88,7 +82,7 @@ const StepAboutRuleComponent: FC = ({ } }, [form]); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView && myStepData.name != null ? ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 6bdef4a69af1e6..5409a5f161bbac 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -11,7 +11,7 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; -import { isEmpty, isEqual, get } from 'lodash/fp'; +import { isEmpty, isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; @@ -19,6 +19,7 @@ import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/pu import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; import { useUiSetting$ } from '../../../../../lib/kibana'; +import { setFieldValue } from '../../helpers'; import * as RuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; @@ -121,14 +122,7 @@ const StepDefineRuleComponent: FC = ({ if (!isEqual(myDefaultValues, myStepData)) { setMyStepData(myDefaultValues); setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + setFieldValue(form, schema, myDefaultValues); } } }, [defaultValues, indicesConfig]); @@ -152,7 +146,7 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView && myStepData?.queryBar != null ? ( = ({ @@ -67,14 +68,7 @@ const StepScheduleRuleComponent: FC = ({ isNew: false, }; setMyStepData(myDefaultValues); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + setFieldValue(form, schema, myDefaultValues); } }, [defaultValues]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 4da17b88b9ad0f..a951c1fab7cc85 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -14,13 +14,14 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', { - defaultMessage: 'Rule run interval & look-back', + defaultMessage: 'Runs every', } ), helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', { - defaultMessage: 'How often and how far back this rule will search specified indices.', + defaultMessage: + 'Rules run periodically and detect signals within the specified time frame.', } ), }, @@ -28,15 +29,14 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', { - defaultMessage: 'Additional look-back', + defaultMessage: 'Additional look-back time', } ), labelAppend: OptionalFieldLabel, helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', { - defaultMessage: - 'Add more time to the look-back range in order to prevent potential gaps in signal reporting.', + defaultMessage: 'Adds time to the look-back period to prevent missed signals.', } ), }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx index feaaf4e85b2af0..67bcc1af8150b9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx @@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n'; export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle', { - defaultMessage: 'Complete rule without activating', + defaultMessage: 'Create rule without activating it', } ); export const COMPLETE_WITH_ACTIVATING = i18n.translate( 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle', { - defaultMessage: 'Complete rule & activate', + defaultMessage: 'Create & activate rule', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index e5656f5b081fb0..cbc60015d9c875 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -9,10 +9,11 @@ import React, { useCallback, useRef, useState } from 'react'; import { Redirect } from 'react-router-dom'; import styled from 'styled-components'; +import { usePersistRule } from '../../../../containers/detection_engine/rules'; import { HeaderPage } from '../../../../components/header_page'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { WrapperPage } from '../../../../components/wrapper_page'; -import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { displaySuccessToast, useStateToaster } from '../../../../components/toasters'; import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; @@ -55,6 +56,7 @@ export const CreateRuleComponent = React.memo(() => { canUserCRUD, hasManageApiKey, } = useUserInfo(); + const [, dispatchToaster] = useStateToaster(); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); @@ -95,6 +97,7 @@ export const CreateRuleComponent = React.memo(() => { const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); if ([0, 1].includes(stepRuleIdx)) { if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); setIsStepRuleInEditView({ ...isStepRuleInReadOnlyView, [step]: true, @@ -203,12 +206,15 @@ export const CreateRuleComponent = React.memo(() => { async (id: RuleStep) => { const activeForm = await stepsForm.current[openAccordionId]?.submit(); if (activeForm != null && activeForm?.isValid) { + stepsData.current[openAccordionId] = { + ...stepsData.current[openAccordionId], + data: activeForm.data, + isValid: activeForm.isValid, + }; setOpenAccordionId(id); - openCloseAccordion(openAccordionId); - setIsStepRuleInEditView({ ...isStepRuleInReadOnlyView, - [openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true, + [openAccordionId]: true, [id]: false, }); } @@ -217,6 +223,8 @@ export const CreateRuleComponent = React.memo(() => { ); if (isSaved) { + const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name; + displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); return ; } @@ -224,7 +232,7 @@ export const CreateRuleComponent = React.memo(() => { <> { { { + i18n.translate('xpack.siem.detectionEngine.rules.create.successfullyCreatedRuleTitle', { + values: { ruleName }, + defaultMessage: '{ruleName} was created', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx index 3b49cd30c9aabc..f660c1763d5e01 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx @@ -15,8 +15,7 @@ import { } from '@elastic/eui'; import React, { memo } from 'react'; -import { useRuleStatus } from '../../../../containers/detection_engine/rules/use_rule_status'; -import { RuleStatus } from '../../../../containers/detection_engine/rules'; +import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import * as i18n from './translations'; import { FormattedDate } from '../../../../components/formatted_date'; @@ -35,7 +34,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => { ); } - const columns: Array> = [ + const columns: Array> = [ { name: i18n.COLUMN_STATUS_TYPE, render: () => {i18n.TYPE_FAILED}, @@ -65,7 +64,9 @@ const FailureHistoryComponent: React.FC = ({ id }) => { rs.last_failure_at != null) : []} + items={ + ruleStatus != null ? ruleStatus?.failures.filter(rs => rs.last_failure_at != null) : [] + } sorting={{ sort: { field: 'status_date', direction: 'desc' } }} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 099006a34920c0..40c694160f73bb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -10,8 +10,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiHealth, EuiTab, + EuiTabs, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { memo, useCallback, useMemo, useState } from 'react'; @@ -60,10 +60,10 @@ import { inputsSelectors } from '../../../../store/inputs'; import { State } from '../../../../store'; import { InputsRange } from '../../../../store/inputs/model'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; -import { getEmptyTagValue } from '../../../../components/empty_value'; +import { RuleActionsOverflow } from '../components/rule_actions_overflow'; import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; -import { RuleActionsOverflow } from '../components/rule_actions_overflow'; +import { RuleStatus } from '../components/rule_status'; interface ReduxProps { filters: esFilters.Filter[]; @@ -78,14 +78,19 @@ export interface DispatchProps { }>; } +enum RuleDetailTabs { + signals = 'signals', + failures = 'failures', +} + const ruleDetailTabs = [ { - id: 'signal', + id: RuleDetailTabs.signals, name: detectionI18n.SIGNAL, disabled: false, }, { - id: 'failure', + id: RuleDetailTabs.failures, name: i18n.FAILURE_HISTORY_TAB, disabled: false, }, @@ -104,9 +109,11 @@ const RuleDetailsComponent = memo( hasIndexWrite, signalIndexName, } = useUserInfo(); - const { ruleId } = useParams(); + const { detailName: ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); - const [ruleDetailTab, setRuleDetailTab] = useState('signal'); + // This is used to re-trigger api rule status when user de/activate rule + const [ruleEnabled, setRuleEnabled] = useState(null); + const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule, detailsView: true, @@ -175,34 +182,28 @@ const RuleDetailsComponent = memo( filters, ]); - const statusColor = - rule?.status == null - ? 'subdued' - : rule?.status === 'succeeded' - ? 'success' - : rule?.status === 'failed' - ? 'danger' - : rule?.status === 'executing' - ? 'warning' - : 'subdued'; - const tabs = useMemo( - () => - ruleDetailTabs.map(tab => ( - setRuleDetailTab(tab.id)} - isSelected={tab.id === ruleDetailTab} - disabled={tab.disabled} - key={tab.name} - > - {tab.name} - - )), + () => ( + + {ruleDetailTabs.map(tab => ( + setRuleDetailTab(tab.id)} + isSelected={tab.id === ruleDetailTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + + ))} + + ), [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] ); const ruleError = useMemo( () => - rule?.status === 'failed' && ruleDetailTab === 'signal' && rule?.last_failure_at != null ? ( + rule?.status === 'failed' && + ruleDetailTab === RuleDetailTabs.signals && + rule?.last_failure_at != null ? ( ( [setAbsoluteRangeDatePicker] ); + const handleOnChangeEnabledRule = useCallback( + (enabled: boolean) => { + if (ruleEnabled == null || enabled !== ruleEnabled) { + setRuleEnabled(enabled); + } + }, + [ruleEnabled, setRuleEnabled] + ); + return ( <> {hasIndexWrite != null && !hasIndexWrite && } @@ -238,7 +248,6 @@ const RuleDetailsComponent = memo( href: `#${DETECTION_ENGINE_PAGE_NAME}/rules`, text: i18n.BACK_TO_RULES, }} - badgeOptions={{ text: i18n.EXPERIMENTAL }} border subtitle={subTitle} subtitle2={[ @@ -251,34 +260,7 @@ const RuleDetailsComponent = memo( , ] : []), - - - {i18n.STATUS} - {':'} - - - - {rule?.status ?? getEmptyTagValue()} - - - {rule?.status_date && ( - <> - - <>{i18n.STATUS_AT} - - - - - - )} - , + , ]} title={title} > @@ -289,6 +271,7 @@ const RuleDetailsComponent = memo( isDisabled={userHasNoPermissions} enabled={rule?.enabled ?? false} optionLabel={i18n.ACTIVATE_RULE} + onChange={handleOnChangeEnabledRule} /> @@ -316,7 +299,7 @@ const RuleDetailsComponent = memo( {ruleError} {tabs} - {ruleDetailTab === 'signal' && ( + {ruleDetailTab === RuleDetailTabs.signals && ( <> @@ -381,7 +364,9 @@ const RuleDetailsComponent = memo( )} )} - {ruleDetailTab === 'failure' && } + {ruleDetailTab === RuleDetailTabs.failures && ( + + )} )} @@ -396,7 +381,7 @@ const RuleDetailsComponent = memo( }} - + ); } @@ -417,8 +402,10 @@ const makeMapStateToProps = () => { }; }; -export const RuleDetails = connect(makeMapStateToProps, { +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -})(RuleDetailsComponent); +}; + +export const RuleDetails = connect(makeMapStateToProps, mapDispatchToProps)(RuleDetailsComponent); RuleDetails.displayName = 'RuleDetails'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts index 9976abc8412bff..46b6984ab323f8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts @@ -13,7 +13,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails export const BACK_TO_RULES = i18n.translate( 'xpack.siem.detectionEngine.ruleDetails.backToRulesDescription', { - defaultMessage: 'Back to rules', + defaultMessage: 'Back to signal detection rules', } ); @@ -35,24 +35,6 @@ export const UNKNOWN = i18n.translate('xpack.siem.detectionEngine.ruleDetails.un defaultMessage: 'Unknown', }); -export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleDetails.statusDescription', { - defaultMessage: 'Status', -}); - -export const STATUS_AT = i18n.translate( - 'xpack.siem.detectionEngine.ruleDetails.statusAtDescription', - { - defaultMessage: 'at', - } -); - -export const STATUS_DATE = i18n.translate( - 'xpack.siem.detectionEngine.ruleDetails.statusDateDescription', - { - defaultMessage: 'Status date', - } -); - export const ERROR_CALLOUT_TITLE = i18n.translate( 'xpack.siem.detectionEngine.ruleDetails.errorCalloutTitle', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index e583461f524397..be56e916ae6c94 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -17,11 +17,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Redirect, useParams } from 'react-router-dom'; +import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; import { HeaderPage } from '../../../../components/header_page'; import { WrapperPage } from '../../../../components/wrapper_page'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; -import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { displaySuccessToast, useStateToaster } from '../../../../components/toasters'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { FormHook, FormData } from '../components/shared_imports'; import { StepPanel } from '../components/step_panel'; @@ -48,6 +49,7 @@ interface ScheduleStepRuleForm extends StepRuleForm { } export const EditRuleComponent = memo(() => { + const [, dispatchToaster] = useStateToaster(); const { loading: initLoading, isSignalIndexExists, @@ -55,7 +57,7 @@ export const EditRuleComponent = memo(() => { canUserCRUD, hasManageApiKey, } = useUserInfo(); - const { ruleId } = useParams(); + const { detailName: ruleId } = useParams(); const [loading, rule] = useRule(ruleId); const userHasNoPermissions = @@ -271,6 +273,7 @@ export const EditRuleComponent = memo(() => { }, []); if (isSaved || (rule != null && rule.immutable)) { + displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); return ; } @@ -344,7 +347,7 @@ export const EditRuleComponent = memo(() => { - + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts index b81ae58e565f0b..f6e56dca19c214 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts @@ -28,3 +28,9 @@ export const SORRY_ERRORS = i18n.translate( export const BACK_TO = i18n.translate('xpack.siem.detectionEngine.editRule.backToDescription', { defaultMessage: 'Back to', }); + +export const SUCCESSFULLY_SAVED_RULE = (ruleName: string) => + i18n.translate('xpack.siem.detectionEngine.rules.update.successfullySavedRuleTitle', { + values: { ruleName }, + defaultMessage: '{ruleName} was saved', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index cc0882dd7e426d..cfe6cb8da1cb09 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick } from 'lodash/fp'; +import { get, pick } from 'lodash/fp'; import { useLocation } from 'react-router-dom'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; +import { FormData, FormHook, FormSchema } from './components/shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; interface GetStepsData { @@ -67,3 +68,15 @@ export const getStepsData = ({ }; export const useQuery = () => new URLSearchParams(useLocation().search); + +export const setFieldValue = ( + form: FormHook, + schema: FormSchema, + defaultValues: unknown +) => + Object.keys(schema).forEach(key => { + const val = get(key, defaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index aeeef925d60e56..d144a6d56a1687 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const BACK_TO_DETECTION_ENGINE = i18n.translate( 'xpack.siem.detectionEngine.rules.backOptionsHeader', { - defaultMessage: 'Back to detection engine', + defaultMessage: 'Back to detections', } ); @@ -18,11 +18,19 @@ export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.impo }); export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', { - defaultMessage: 'Add new rule', + defaultMessage: 'Create new rule', }); export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { - defaultMessage: 'Rules', + defaultMessage: 'Signal detection rules', +}); + +export const ADD_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.addPageTitle', { + defaultMessage: 'Create', +}); + +export const EDIT_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.editPageTitle', { + defaultMessage: 'Edit', }); export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', { @@ -32,7 +40,7 @@ export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules export const BATCH_ACTIONS = i18n.translate( 'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle', { - defaultMessage: 'Batch actions', + defaultMessage: 'Bulk actions', } ); @@ -75,10 +83,10 @@ export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( } ); -export const BATCH_ACTION_EDIT_INDEX_PATTERNS = i18n.translate( - 'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle', +export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle', { - defaultMessage: 'Edit selected index patterns…', + defaultMessage: 'Duplicate selected…', } ); @@ -243,7 +251,7 @@ export const COLUMN_TAGS = i18n.translate( export const COLUMN_ACTIVATE = i18n.translate( 'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle', { - defaultMessage: 'Activate', + defaultMessage: 'Activated', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts new file mode 100644 index 00000000000000..55772aa73ecf33 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.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 { Breadcrumb } from 'ui/chrome'; +import { isEmpty } from 'lodash/fp'; + +import { + getDetectionEngineUrl, + getDetectionEngineTabUrl, + getRulesUrl, + getRuleDetailsUrl, + getCreateRuleUrl, + getEditRuleUrl, +} from '../../../components/link_to/redirect_to_detection_engine'; +import * as i18nDetections from '../translations'; +import * as i18nRules from './translations'; +import { RouteSpyState } from '../../../utils/route/types'; + +const getTabBreadcrumb = (pathname: string, search: string[]) => { + const tabPath = pathname.split('/')[2]; + + if (tabPath === 'alerts') { + return { + text: i18nDetections.ALERT, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'signals') { + return { + text: i18nDetections.SIGNAL, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'rules') { + return { + text: i18nRules.PAGE_TITLE, + href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } +}; + +const isRuleCreatePage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/create'); + +const isRuleEditPage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/edit'); + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { + let breadcrumb = [ + { + text: i18nDetections.PAGE_TITLE, + href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); + + if (tabBreadcrumb) { + breadcrumb = [...breadcrumb, tabBreadcrumb]; + } + + if (params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.ruleName, + href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleCreatePage(params.pathName)) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.ADD_PAGE_TITLE, + href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.EDIT_PAGE_TITLE, + href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index e5f830d3a49b0e..ab785a8ad2c6d8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; -export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', { - defaultMessage: 'Detection engine', +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.detectionsPageTitle', { + defaultMessage: 'Detections', }); export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', { @@ -22,8 +22,12 @@ export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', { defaultMessage: 'Signals', }); +export const ALERT = i18n.translate('xpack.siem.detectionEngine.alertTitle', { + defaultMessage: 'Third-party alerts', +}); + export const BUTTON_MANAGE_RULES = i18n.translate('xpack.siem.detectionEngine.buttonManageRules', { - defaultMessage: 'Manage rules', + defaultMessage: 'Manage signal detection rules', }); export const PANEL_SUBTITLE_SHOWING = i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts new file mode 100644 index 00000000000000..d529d99ad3ad4b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum DetectionEngineTab { + signals = 'signals', + alerts = 'alerts', +} diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index 220f8a958aa43b..c0e959c5e97fa9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -36,12 +36,12 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'network', }, - [SiemPageName.detectionEngine]: { - id: SiemPageName.detectionEngine, + [SiemPageName.detections]: { + id: SiemPageName.detections, name: i18n.DETECTION_ENGINE, href: getDetectionEngineUrl(), disabled: false, - urlKey: 'detection-engine', + urlKey: 'detections', }, [SiemPageName.timelines]: { id: SiemPageName.timelines, diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index a545be447796dc..b5bfdbde306caf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -105,7 +105,7 @@ export const HomePage: React.FC = () => ( )} /> ( )} diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index b87ea1c17a117e..80800a3bd4198d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -19,7 +19,7 @@ export const NETWORK = i18n.translate('xpack.siem.navigation.network', { }); export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionEngine', { - defaultMessage: 'Detection engine', + defaultMessage: 'Detections', }); export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/legacy/plugins/siem/public/pages/home/types.ts index 101c6a69b08d19..678de6dbcc1284 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/types.ts @@ -10,7 +10,7 @@ export enum SiemPageName { overview = 'overview', hosts = 'hosts', network = 'network', - detectionEngine = 'detection-engine', + detections = 'detections', timelines = 'timelines', } @@ -18,7 +18,7 @@ export type SiemNavTabKey = | SiemPageName.overview | SiemPageName.hosts | SiemPageName.network - | SiemPageName.detectionEngine + | SiemPageName.detections | SiemPageName.timelines; export type SiemNavTab = Record; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts index 52e016502940b1..c321478f101741 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts @@ -5,8 +5,8 @@ */ import { Breadcrumb } from 'ui/chrome'; +import { get, isEmpty } from 'lodash/fp'; -import { get } from 'lodash/fp'; import { hostsModel } from '../../../store'; import { HostsTableType } from '../../../store/hosts/model'; import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; @@ -29,7 +29,7 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Bre let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: `${getHostsUrl()}${search && search[0] ? search[0] : ''}`, + href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, }, ]; @@ -38,7 +38,7 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Bre ...breadcrumb, { text: params.detailName, - href: `${getHostDetailsUrl(params.detailName)}${search && search[1] ? search[1] : ''}`, + href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, }, ]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx index 0bb95632963160..0109eeef914635 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx @@ -25,7 +25,7 @@ const AuthenticationTableManage = manageQuery(AuthenticationTable); const ID = 'authenticationsOverTimeQuery'; const authStackByOptions: MatrixHistogramOption[] = [ { - text: i18n.NAVIGATION_AUTHENTICATIONS_STACK_BY_EVENT_TYPE, + text: 'event.type', value: 'event.type', }, ]; @@ -71,7 +71,6 @@ export const AuthenticationsQueryTabBody = ({ isAuthenticationsHistogram={true} dataKey="AuthenticationsHistogram" defaultStackByOption={authStackByOptions[0]} - deleteQuery={deleteQuery} endDate={endDate} errorMessage={i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA} filterQuery={filterQuery} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx index a07cbc8484a1b4..85bca90cc8e04a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx @@ -20,11 +20,11 @@ const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; export const eventsStackByOptions: MatrixHistogramOption[] = [ { - text: i18n.NAVIGATION_EVENTS_STACK_BY_EVENT_ACTION, + text: 'event.action', value: 'event.action', }, { - text: i18n.NAVIGATION_EVENTS_STACK_BY_EVENT_DATASET, + text: 'event.dataset', value: 'event.dataset', }, ]; @@ -50,7 +50,6 @@ export const EventsQueryTabBody = ({ void; filters?: esFilters.Filter[]; from: number; + hideHeaderChildren?: boolean; indexPattern: IIndexPattern; query?: Query; setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; @@ -60,14 +60,24 @@ export const AlertsByCategory = React.memo( deleteQuery, filters = NO_FILTERS, from, + hideHeaderChildren = false, indexPattern, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, setQuery, to, }) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const updateDateRangeCallback = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); @@ -76,17 +86,11 @@ export const AlertsByCategory = React.memo( ); const alertsCountViewAlertsButton = useMemo( () => ( - - {i18n.VIEW_ALERTS} - + {i18n.VIEW_ALERTS} ), [] ); - const getTitle = useCallback( - (option: MatrixHistogramOption) => i18n.ALERTS_COUNT_BY(option.text), - [] - ); const getSubtitle = useCallback( (totalCount: number) => `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, @@ -96,7 +100,6 @@ export const AlertsByCategory = React.memo( return ( ( queries: [query], filters, })} - headerChildren={alertsCountViewAlertsButton} + headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} isAlertsHistogram={true} legendPosition={'right'} @@ -115,7 +118,7 @@ export const AlertsByCategory = React.memo( sourceId="default" stackByOptions={alertsStackByOptions} startDate={from} - title={getTitle} + title={i18n.ALERTS_GRAPH_TITLE} subtitle={getSubtitle} type={HostsType.page} updateDateRange={updateDateRangeCallback} diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index 52084c4bfc2804..191b4a25926955 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -6,7 +6,7 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public'; import styled from 'styled-components'; @@ -66,8 +66,17 @@ export const EventsByDataset = React.memo( setQuery, to, }) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const updateDateRangeCallback = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); @@ -96,7 +105,6 @@ export const EventsByDataset = React.memo( return ( defaultMessage: 'Alerts count by {groupByField}', }); +export const ALERTS_GRAPH_TITLE = i18n.translate('xpack.siem.overview.alertsGraphTitle', { + defaultMessage: 'Alert detection frequency', +}); + export const EVENTS_COUNT_BY = (groupByField: string) => i18n.translate('xpack.siem.overview.eventsCountByTitle', { values: { groupByField }, diff --git a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts index 188ae9c6c1866d..39efccc9f45b85 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts @@ -15,6 +15,7 @@ export const initRouteSpy: RouteSpyState = { tabName: undefined, search: '', pathName: '/', + state: undefined, }; export const RouterSpyStateContext = createContext<[RouteSpyState, Dispatch]>([ diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx index 5c24b2f48488d8..c88562abef6ae0 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -8,6 +8,7 @@ import * as H from 'history'; import { isEqual } from 'lodash/fp'; import { memo, useEffect, useState } from 'react'; import { withRouter } from 'react-router-dom'; +import deepEqual from 'fast-deep-equal'; import { SpyRouteProps } from './types'; import { useRouteSpy } from './use_route_spy'; @@ -19,6 +20,7 @@ export const SpyRouteComponent = memo( match: { params: { pageName, detailName, tabName, flowTarget }, }, + state, }) => { const [isInitializing, setIsInitializing] = useState(true); const [route, dispatch] = useRouteSpy(); @@ -61,8 +63,24 @@ export const SpyRouteComponent = memo( }, }); } + } else { + if (pageName && !deepEqual(state, route.state)) { + dispatch({ + type: 'updateRoute', + route: { + pageName, + detailName, + tabName, + search, + pathName: pathname, + history, + flowTarget, + state, + }, + }); + } } - }, [pathname, search, pageName, detailName, tabName, flowTarget]); + }, [pathname, search, pageName, detailName, tabName, flowTarget, state]); return null; } ); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/legacy/plugins/siem/public/utils/route/types.ts index 79d2677eff06f6..d3eca36bd0d96d 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/types.ts +++ b/x-pack/legacy/plugins/siem/public/utils/route/types.ts @@ -21,6 +21,7 @@ export interface RouteSpyState { pathName: string; history?: H.History; flowTarget?: FlowTarget; + state?: Record; } export interface HostRouteSpyState extends RouteSpyState { @@ -38,7 +39,10 @@ export type RouteSpyAction = } | { type: 'updateRouteWithOutSearch'; - route: Pick; + route: Pick< + RouteSpyState, + 'pageName' & 'detailName' & 'tabName' & 'pathName' & 'history' & 'state' + >; } | { type: 'updateRoute'; @@ -55,4 +59,6 @@ export type SpyRouteProps = RouteComponentProps<{ tabName: HostsTableType | undefined; search: string; flowTarget: FlowTarget | undefined; -}>; +}> & { + state?: Record; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 30a8d9d9351289..a84fcb64d9ff7c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -18,9 +18,9 @@ import { DETECTION_ENGINE_PREPACKAGED_URL, } from '../../../../../common/constants'; import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; -import { RuleAlertParamsRest } from '../../types'; +import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; -export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({ +export const mockPrepackagedRule = (): PrepackagedRules => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -51,8 +51,6 @@ export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({ false_positives: [], saved_id: 'some-id', max_signals: 100, - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', timeline_id: 'timeline-id', timeline_title: 'timeline-title', }); @@ -393,7 +391,7 @@ export const getMockPrivileges = () => ({ }, }, application: {}, - isAuthenticated: false, + is_authenticated: false, }); export const getFindResultStatus = (): SavedObjectsFindResponse => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 240200af8b5855..803d9d645aadbe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -30,7 +30,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); return merge(permissions, { - isAuthenticated: request?.auth?.isAuthenticated ?? false, + is_authenticated: request?.auth?.isAuthenticated ?? false, }); } catch (err) { return transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 5ceecdb058e5f2..3c9cad8dc4d4b1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -36,8 +36,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } @@ -59,7 +61,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR } } await installPrepackagedRules(alertsClient, actionsClient, rulesToInstall, spaceIndex); - await updatePrepackagedRules(alertsClient, actionsClient, rulesToUpdate, spaceIndex); + await updatePrepackagedRules( + alertsClient, + actionsClient, + savedObjectsClient, + rulesToUpdate, + spaceIndex + ); return { rules_installed: rulesToInstall.length, rules_updated: rulesToUpdate.length, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 9c18f9040008c7..00a1d2eb980ec3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -55,7 +55,6 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -109,7 +108,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, falsePositives, from, - immutable, + immutable: false, query, language, outputIndex: finalIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index aa535d325f4b96..23acd12d341ed8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -39,7 +39,6 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -96,7 +95,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = enabled, falsePositives, from, - immutable, + immutable: false, query, language, outputIndex: finalIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index e56c440f5a415e..545c2e488b1c8b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -13,10 +13,16 @@ import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequest, IRuleSavedAttributesSavedObjectAttributes, + RuleStatusResponse, + IRuleStatusAttributes, } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -const convertToSnakeCase = (obj: IRuleSavedAttributesSavedObjectAttributes) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const convertToSnakeCase = >(obj: T): Partial | null => { + if (!obj) { + return null; + } return Object.keys(obj).reduce((acc, item) => { const newKey = snakeCase(item); return { ...acc, [newKey]: obj[item] }; @@ -53,7 +59,7 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { "anotherAlertId": ... } */ - const statuses = await query.ids.reduce(async (acc, id) => { + const statuses = await query.ids.reduce>(async (acc, id) => { const lastFiveErrorsForId = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -64,15 +70,21 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { search: id, searchFields: ['alertId'], }); - const toDisplay = - lastFiveErrorsForId.saved_objects.length <= 5 - ? lastFiveErrorsForId.saved_objects - : lastFiveErrorsForId.saved_objects.slice(1); + const accumulated = await acc; + const currentStatus = convertToSnakeCase( + lastFiveErrorsForId.saved_objects[0]?.attributes + ); + const failures = lastFiveErrorsForId.saved_objects + .slice(1) + .map(errorItem => convertToSnakeCase(errorItem.attributes)); return { - ...(await acc), - [id]: toDisplay.map(errorItem => convertToSnakeCase(errorItem.attributes)), + ...accumulated, + [id]: { + current_status: currentStatus, + failures, + }, }; - }, {}); + }, Promise.resolve({})); return statuses; }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index e312b5fc6bb102..6efaa1fea60d08 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -52,8 +52,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } const { filename } = request.payload.file.hapi; @@ -161,6 +163,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const updatedRule = await updateRules({ alertsClient, actionsClient, + savedObjectsClient, description, enabled, falsePositives, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 180a75bdaaeead..e0d2672cf356a5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -7,12 +7,16 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { BulkUpdateRulesRequest } from '../../rules/types'; +import { + BulkUpdateRulesRequest, + IRuleSavedAttributesSavedObjectAttributes, +} from '../../rules/types'; import { ServerFacade } from '../../../../types'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { updateRules } from '../../rules/update_rules'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -32,8 +36,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } @@ -44,7 +50,6 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -77,11 +82,11 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, falsePositives, from, - immutable, query, language, outputIndex, savedId, + savedObjectsClient, timelineId, timelineTitle, meta, @@ -102,7 +107,17 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou version, }); if (rule != null) { - return transformOrBulkError(rule.id, rule); + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 147f3f9afa549e..49c9304ae2d25a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -33,7 +33,6 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -75,11 +74,11 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { enabled, falsePositives, from, - immutable, query, language, outputIndex, savedId, + savedObjectsClient, timelineId, timelineTitle, meta, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 1993948808ef49..abdd5a0c7b5084 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -4,20 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpdateRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; describe('add prepackaged rules schema', () => { test('empty objects do not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({}).error - ).toBeTruthy(); + expect(addPrepackagedRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -25,7 +22,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -33,7 +30,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -42,7 +39,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -52,7 +49,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -63,7 +60,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -75,7 +72,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -88,7 +85,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -102,7 +99,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -117,7 +114,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -133,7 +130,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval, version] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -152,7 +149,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -170,7 +167,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, version] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -190,7 +187,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does not validate because output_index is not allowed', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -211,7 +208,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -229,7 +226,7 @@ describe('add prepackaged rules schema', () => { test('You can send in an empty array to threats', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -251,7 +248,7 @@ describe('add prepackaged rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version, threats] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -286,7 +283,7 @@ describe('add prepackaged rules schema', () => { test('allows references to be sent as valid', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -307,7 +304,7 @@ describe('add prepackaged rules schema', () => { test('defaults references to an array', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -327,7 +324,7 @@ describe('add prepackaged rules schema', () => { test('defaults immutable to true', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -347,7 +344,7 @@ describe('add prepackaged rules schema', () => { test('immutable cannot be false', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -368,7 +365,7 @@ describe('add prepackaged rules schema', () => { test('immutable can be true', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -389,7 +386,7 @@ describe('add prepackaged rules schema', () => { test('defaults enabled to false', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -409,7 +406,7 @@ describe('add prepackaged rules schema', () => { test('rule_id is required', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ risk_score: 50, description: 'some description', from: 'now-5m', @@ -429,7 +426,7 @@ describe('add prepackaged rules schema', () => { test('references cannot be numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { references: number[] } + Partial> & { references: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -454,7 +451,7 @@ describe('add prepackaged rules schema', () => { test('indexes cannot be numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { index: number[] } + Partial> & { index: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -477,7 +474,7 @@ describe('add prepackaged rules schema', () => { test('defaults interval to 5 min', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -494,7 +491,7 @@ describe('add prepackaged rules schema', () => { test('defaults max signals to 100', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -512,7 +509,7 @@ describe('add prepackaged rules schema', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -530,7 +527,7 @@ describe('add prepackaged rules schema', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -549,7 +546,7 @@ describe('add prepackaged rules schema', () => { test('saved_query type can have filters with it', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -570,7 +567,7 @@ describe('add prepackaged rules schema', () => { test('filters cannot be a string', () => { expect( addPrepackagedRulesSchema.validate< - Partial & { filters: string }> + Partial & { filters: string }> >({ rule_id: 'rule-1', risk_score: 50, @@ -591,7 +588,7 @@ describe('add prepackaged rules schema', () => { test('language validates with kuery', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -612,7 +609,7 @@ describe('add prepackaged rules schema', () => { test('language validates with lucene', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -633,7 +630,7 @@ describe('add prepackaged rules schema', () => { test('language does not validate with something made up', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -654,7 +651,7 @@ describe('add prepackaged rules schema', () => { test('max_signals cannot be negative', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -676,7 +673,7 @@ describe('add prepackaged rules schema', () => { test('max_signals cannot be zero', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -698,7 +695,7 @@ describe('add prepackaged rules schema', () => { test('max_signals can be 1', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -720,7 +717,7 @@ describe('add prepackaged rules schema', () => { test('You can optionally send in an array of tags', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -744,7 +741,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of tags that are numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { tags: number[] } + Partial> & { tags: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -771,7 +768,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of threats that are missing "framework"', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -815,7 +812,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of threats that are missing "tactic"', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -855,7 +852,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of threats that are missing "techniques"', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -892,7 +889,7 @@ describe('add prepackaged rules schema', () => { test('You can optionally send in an array of false positives', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -916,7 +913,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { false_positives: number[] } + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -942,7 +939,7 @@ describe('add prepackaged rules schema', () => { test('You can optionally set the immutable to be true', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -966,7 +963,7 @@ describe('add prepackaged rules schema', () => { test('You cannot set the immutable to be a number', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { immutable: number } + Partial> & { immutable: number } >({ rule_id: 'rule-1', risk_score: 50, @@ -990,7 +987,7 @@ describe('add prepackaged rules schema', () => { test('You cannot set the risk_score to 101', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 101, description: 'some description', @@ -1013,7 +1010,7 @@ describe('add prepackaged rules schema', () => { test('You cannot set the risk_score to -1', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: -1, description: 'some description', @@ -1036,7 +1033,7 @@ describe('add prepackaged rules schema', () => { test('You can set the risk_score to 0', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 0, description: 'some description', @@ -1059,7 +1056,7 @@ describe('add prepackaged rules schema', () => { test('You can set the risk_score to 100', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 100, description: 'some description', @@ -1082,7 +1079,7 @@ describe('add prepackaged rules schema', () => { test('You can set meta to any object you want', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1109,7 +1106,7 @@ describe('add prepackaged rules schema', () => { test('You cannot create meta as a string', () => { expect( addPrepackagedRulesSchema.validate< - Partial & { meta: string }> + Partial & { meta: string }> >({ rule_id: 'rule-1', risk_score: 50, @@ -1134,7 +1131,7 @@ describe('add prepackaged rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1157,7 +1154,7 @@ describe('add prepackaged rules schema', () => { test('validates with timeline_id and timeline_title', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1180,7 +1177,7 @@ describe('add prepackaged rules schema', () => { test('You cannot omit timeline_title when timeline_id is present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1204,7 +1201,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have a null value for timeline_title when timeline_id is present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1229,7 +1226,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have empty string for timeline_title when timeline_id is present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1254,7 +1251,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have timeline_title with an empty timeline_id', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1279,7 +1276,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have timeline_title without timeline_id', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 15f4fa7f056484..c76071047434c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -884,7 +884,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -907,7 +906,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -999,7 +997,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index bed64cc6e7a02a..20f418c57b5dbe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -9,18 +9,18 @@ import { importRulesQuerySchema, importRulesPayloadSchema, } from './import_rules_schema'; -import { ThreatParams, RuleAlertParamsRest, ImportRuleAlertRest } from '../../types'; +import { ThreatParams, ImportRuleAlertRest } from '../../types'; import { ImportRulesRequest } from '../../rules/types'; describe('import rules schema', () => { describe('importRulesSchema', () => { test('empty objects do not validate', () => { - expect(importRulesSchema.validate>({}).error).toBeTruthy(); + expect(importRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -28,7 +28,7 @@ describe('import rules schema', () => { test('[rule_id] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -36,7 +36,7 @@ describe('import rules schema', () => { test('[rule_id, description] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -45,7 +45,7 @@ describe('import rules schema', () => { test('[rule_id, description, from] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -55,7 +55,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -66,7 +66,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -78,7 +78,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -91,7 +91,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -105,7 +105,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -120,7 +120,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -136,7 +136,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -154,7 +154,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -172,7 +172,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -191,7 +191,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -211,7 +211,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -228,7 +228,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -246,7 +246,7 @@ describe('import rules schema', () => { test('You can send in an empty array to threats', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -269,7 +269,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -304,7 +304,7 @@ describe('import rules schema', () => { test('allows references to be sent as valid', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -325,7 +325,7 @@ describe('import rules schema', () => { test('defaults references to an array', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -346,7 +346,7 @@ describe('import rules schema', () => { test('references cannot be numbers', () => { expect( importRulesSchema.validate< - Partial> & { references: number[] } + Partial> & { references: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -371,7 +371,7 @@ describe('import rules schema', () => { test('indexes cannot be numbers', () => { expect( importRulesSchema.validate< - Partial> & { index: number[] } + Partial> & { index: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -394,7 +394,7 @@ describe('import rules schema', () => { test('defaults interval to 5 min', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -411,7 +411,7 @@ describe('import rules schema', () => { test('defaults max signals to 100', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -429,7 +429,7 @@ describe('import rules schema', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -447,7 +447,7 @@ describe('import rules schema', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -466,7 +466,7 @@ describe('import rules schema', () => { test('saved_query type can have filters with it', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -487,7 +487,7 @@ describe('import rules schema', () => { test('filters cannot be a string', () => { expect( importRulesSchema.validate< - Partial & { filters: string }> + Partial & { filters: string }> >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -508,7 +508,7 @@ describe('import rules schema', () => { test('language validates with kuery', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -529,7 +529,7 @@ describe('import rules schema', () => { test('language validates with lucene', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -550,7 +550,7 @@ describe('import rules schema', () => { test('language does not validate with something made up', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -571,7 +571,7 @@ describe('import rules schema', () => { test('max_signals cannot be negative', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -593,7 +593,7 @@ describe('import rules schema', () => { test('max_signals cannot be zero', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -615,7 +615,7 @@ describe('import rules schema', () => { test('max_signals can be 1', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -637,7 +637,7 @@ describe('import rules schema', () => { test('You can optionally send in an array of tags', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -660,7 +660,7 @@ describe('import rules schema', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - importRulesSchema.validate> & { tags: number[] }>( + importRulesSchema.validate> & { tags: number[] }>( { rule_id: 'rule-1', output_index: '.siem-signals', @@ -688,7 +688,7 @@ describe('import rules schema', () => { test('You cannot send in an array of threats that are missing "framework"', () => { expect( importRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -732,7 +732,7 @@ describe('import rules schema', () => { test('You cannot send in an array of threats that are missing "tactic"', () => { expect( importRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -772,7 +772,7 @@ describe('import rules schema', () => { test('You cannot send in an array of threats that are missing "techniques"', () => { expect( importRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -809,7 +809,7 @@ describe('import rules schema', () => { test('You can optionally send in an array of false positives', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -833,7 +833,7 @@ describe('import rules schema', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( importRulesSchema.validate< - Partial> & { false_positives: number[] } + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -859,7 +859,7 @@ describe('import rules schema', () => { test('You can optionally set the immutable to be true', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -883,7 +883,7 @@ describe('import rules schema', () => { test('You cannot set the immutable to be a number', () => { expect( importRulesSchema.validate< - Partial> & { immutable: number } + Partial> & { immutable: number } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -907,7 +907,7 @@ describe('import rules schema', () => { test('You cannot set the risk_score to 101', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 101, @@ -930,7 +930,7 @@ describe('import rules schema', () => { test('You cannot set the risk_score to -1', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: -1, @@ -953,7 +953,7 @@ describe('import rules schema', () => { test('You can set the risk_score to 0', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 0, @@ -976,7 +976,7 @@ describe('import rules schema', () => { test('You can set the risk_score to 100', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 100, @@ -999,7 +999,7 @@ describe('import rules schema', () => { test('You can set meta to any object you want', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1025,7 +1025,7 @@ describe('import rules schema', () => { test('You cannot create meta as a string', () => { expect( - importRulesSchema.validate & { meta: string }>>({ + importRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1049,7 +1049,7 @@ describe('import rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1072,7 +1072,7 @@ describe('import rules schema', () => { test('validates with timeline_id and timeline_title', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1095,7 +1095,7 @@ describe('import rules schema', () => { test('You cannot omit timeline_title when timeline_id is present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1117,7 +1117,7 @@ describe('import rules schema', () => { test('You cannot have a null value for timeline_title when timeline_id is present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1140,7 +1140,7 @@ describe('import rules schema', () => { test('You cannot have empty string for timeline_title when timeline_id is present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1165,7 +1165,7 @@ describe('import rules schema', () => { test('You cannot have timeline_title with an empty timeline_id', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1188,7 +1188,7 @@ describe('import rules schema', () => { test('You cannot have timeline_title without timeline_id', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts index 260147ed0506c8..8ca07caef0c7f0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts @@ -5,7 +5,7 @@ */ import { getPrepackagedRules } from './get_prepackaged_rules'; -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { isEmpty } from 'lodash/fp'; describe('get_existing_prepackaged_rules', () => { @@ -15,7 +15,7 @@ describe('get_existing_prepackaged_rules', () => { test('no rule should have the same rule_id as another rule_id', () => { const prePacakgedRules = getPrepackagedRules(); - let existingRuleIds: RuleAlertParamsRest[] = []; + let existingRuleIds: PrepackagedRules[] = []; prePacakgedRules.forEach(rule => { const foundDuplicate = existingRuleIds.reduce((accum, existingRule) => { if (existingRule.rule_id === rule.rule_id) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts index 855d0d73f6796a..bcfe6ee203ecd1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { addPrepackagedRulesSchema } from '../routes/schemas/add_prepackaged_rules_schema'; import { rawRules } from './prepackaged_rules'; @@ -13,9 +13,7 @@ import { rawRules } from './prepackaged_rules'; * that they are adding incorrect schema rules. Also this will auto-flush in all the default * aspects such as default interval of 5 minutes, default arrays, etc... */ -export const validateAllPrepackagedRules = ( - rules: RuleAlertParamsRest[] -): RuleAlertParamsRest[] => { +export const validateAllPrepackagedRules = (rules: PrepackagedRules[]): PrepackagedRules[] => { return rules.map(rule => { const validatedRule = addPrepackagedRulesSchema.validate(rule); if (validatedRule.error != null) { @@ -35,6 +33,6 @@ export const validateAllPrepackagedRules = ( }); }; -export const getPrepackagedRules = (rules = rawRules): RuleAlertParamsRest[] => { +export const getPrepackagedRules = (rules = rawRules): PrepackagedRules[] => { return validateAllPrepackagedRules(rules); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts index 1a2bd4a10ac2de..ee76bf2ef15b89 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts @@ -5,7 +5,7 @@ */ import { getRulesToInstall } from './get_rules_to_install'; -import { getResult, fullRuleAlertParamsRest } from '../routes/__mocks__/request_responses'; +import { getResult, mockPrepackagedRule } from '../routes/__mocks__/request_responses'; describe('get_rules_to_install', () => { test('should return empty array if both rule sets are empty', () => { @@ -14,7 +14,7 @@ describe('get_rules_to_install', () => { }); test('should return empty array if the two rule ids match', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; const installedRule = getResult(); @@ -24,7 +24,7 @@ describe('get_rules_to_install', () => { }); test('should return the rule to install if the id of the two rules do not match', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; const installedRule = getResult(); @@ -34,10 +34,10 @@ describe('get_rules_to_install', () => { }); test('should return two rules to install if both the ids of the two rules do not match', () => { - const ruleFromFileSystem1 = fullRuleAlertParamsRest(); + const ruleFromFileSystem1 = mockPrepackagedRule(); ruleFromFileSystem1.rule_id = 'rule-1'; - const ruleFromFileSystem2 = fullRuleAlertParamsRest(); + const ruleFromFileSystem2 = mockPrepackagedRule(); ruleFromFileSystem2.rule_id = 'rule-2'; const installedRule = getResult(); @@ -47,13 +47,13 @@ describe('get_rules_to_install', () => { }); test('should return two rules of three to install if both the ids of the two rules do not match but the third does', () => { - const ruleFromFileSystem1 = fullRuleAlertParamsRest(); + const ruleFromFileSystem1 = mockPrepackagedRule(); ruleFromFileSystem1.rule_id = 'rule-1'; - const ruleFromFileSystem2 = fullRuleAlertParamsRest(); + const ruleFromFileSystem2 = mockPrepackagedRule(); ruleFromFileSystem2.rule_id = 'rule-2'; - const ruleFromFileSystem3 = fullRuleAlertParamsRest(); + const ruleFromFileSystem3 = mockPrepackagedRule(); ruleFromFileSystem3.rule_id = 'rule-3'; const installedRule = getResult(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts index 1c795941cbb837..c44e4fb812c35c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { RuleAlertType } from './types'; export const getRulesToInstall = ( - rulesFromFileSystem: RuleAlertParamsRest[], + rulesFromFileSystem: PrepackagedRules[], installedRules: RuleAlertType[] -): RuleAlertParamsRest[] => { +): PrepackagedRules[] => { return rulesFromFileSystem.filter( rule => !installedRules.some(installedRule => installedRule.params.ruleId === rule.rule_id) ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts index 7f1b64d33cd9bf..40e303bddac1ae 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts @@ -5,7 +5,7 @@ */ import { getRulesToUpdate } from './get_rules_to_update'; -import { getResult, fullRuleAlertParamsRest } from '../routes/__mocks__/request_responses'; +import { getResult, mockPrepackagedRule } from '../routes/__mocks__/request_responses'; describe('get_rules_to_update', () => { test('should return empty array if both rule sets are empty', () => { @@ -14,7 +14,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of the two rules do not match', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -26,7 +26,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of file system rule is less than the installed version', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 1; @@ -38,7 +38,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of file system rule is the same as the installed version', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 1; @@ -50,7 +50,7 @@ describe('get_rules_to_update', () => { }); test('should return the rule to update if the id of file system rule is greater than the installed version', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -62,7 +62,7 @@ describe('get_rules_to_update', () => { }); test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -79,11 +79,11 @@ describe('get_rules_to_update', () => { }); test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => { - const ruleFromFileSystem1 = fullRuleAlertParamsRest(); + const ruleFromFileSystem1 = mockPrepackagedRule(); ruleFromFileSystem1.rule_id = 'rule-1'; ruleFromFileSystem1.version = 2; - const ruleFromFileSystem2 = fullRuleAlertParamsRest(); + const ruleFromFileSystem2 = mockPrepackagedRule(); ruleFromFileSystem2.rule_id = 'rule-2'; ruleFromFileSystem2.version = 2; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts index 10b849493858a1..31eff6a4ec87a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { RuleAlertType } from './types'; export const getRulesToUpdate = ( - rulesFromFileSystem: RuleAlertParamsRest[], + rulesFromFileSystem: PrepackagedRules[], installedRules: RuleAlertType[] -): RuleAlertParamsRest[] => { +): PrepackagedRules[] => { return rulesFromFileSystem.filter(rule => installedRules.some(installedRule => { return ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 9c3be64f71a0dc..98c04f95387f42 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -7,12 +7,12 @@ import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; import { createRules } from './create_rules'; -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; export const installPrepackagedRules = async ( alertsClient: AlertsClient, actionsClient: ActionsClient, - rules: RuleAlertParamsRest[], + rules: PrepackagedRules[], outputIndex: string ): Promise => { await rules.forEach(async rule => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 5a3f19c0bf0ef8..e238e6398845cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -7,7 +7,12 @@ import { get } from 'lodash/fp'; import { Readable } from 'stream'; -import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server'; +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsFindResponse, + SavedObjectsClientContract, +} from 'kibana/server'; import { SIGNALS_ID } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting/server/alerts_client'; import { ActionsClient } from '../../../../../actions/server/actions_client'; @@ -41,14 +46,22 @@ export interface RuleAlertType extends Alert { params: RuleTypeParams; } -export interface IRuleStatusAttributes { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IRuleStatusAttributes extends Record { alertId: string; // created alert id. statusDate: string; lastFailureAt: string | null | undefined; lastFailureMessage: string | null | undefined; lastSuccessAt: string | null | undefined; lastSuccessMessage: string | null | undefined; - status: RuleStatusString; + status: RuleStatusString | null | undefined; +} + +export interface RuleStatusResponse { + [key: string]: { + current_status: IRuleStatusAttributes | null | undefined; + failures: IRuleStatusAttributes[] | null | undefined; + }; } export interface IRuleSavedAttributesSavedObjectAttributes @@ -142,6 +155,7 @@ export interface Clients { export type UpdateRuleParams = Partial & { id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; } & Clients; export type DeleteRuleParams = Clients & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 3d2ca8f91281b8..0d7fb7918b67e6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; import { updateRules } from './update_rules'; -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; export const updatePrepackagedRules = async ( alertsClient: AlertsClient, actionsClient: ActionsClient, - rules: RuleAlertParamsRest[], + savedObjectsClient: SavedObjectsClientContract, + rules: PrepackagedRules[], outputIndex: string ): Promise => { await rules.forEach(async rule => { @@ -55,6 +57,7 @@ export const updatePrepackagedRules = async ( outputIndex, id: undefined, // We never have an id when updating from pre-packaged rules savedId, + savedObjectsClient, meta, filters, ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 0fe4b15437af85..e2632791f859e3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -7,8 +7,9 @@ import { defaults } from 'lodash/fp'; import { AlertAction, IntervalSchedule } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; -import { UpdateRuleParams } from './types'; +import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; import { addTags } from './add_tags'; +import { ruleStatusSavedObjectType } from './saved_object_mappings'; export const calculateInterval = ( interval: string | undefined, @@ -66,6 +67,7 @@ export const calculateName = ({ export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types + savedObjectsClient, description, falsePositives, enabled, @@ -135,10 +137,39 @@ export const updateRules = async ({ } ); + const ruleCurrentStatus = savedObjectsClient + ? await savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }) + : null; + if (rule.enabled && enabled === false) { await alertsClient.disable({ id: rule.id }); + // set current status for this rule to null to represent disabled, + // but keep last_success_at / last_failure_at properties intact for + // use on frontend while rule is disabled. + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = null; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); + // set current status for this rule to be 'going to run' + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = 'going to run'; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index d80eadd2c088ba..32f2c86914770d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -96,7 +96,7 @@ export const signalRulesAlertType = ({ >(ruleStatusSavedObjectType, { alertId, // do a search for this id. statusDate: date, - status: 'executing', + status: 'going to run', lastFailureAt: null, lastSuccessAt: null, lastFailureMessage: null, @@ -106,7 +106,7 @@ export const signalRulesAlertType = ({ // update 0th to executing. currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'executing'; + currentStatusSavedObject.attributes.status = 'going to run'; currentStatusSavedObject.attributes.statusDate = sDate; await services.savedObjectsClient.update( ruleStatusSavedObjectType, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 8a9e050c039b4f..c7bd92322360a8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -58,6 +58,7 @@ export type RuleAlertParamsRest = Omit< RuleAlertParams, | 'ruleId' | 'falsePositives' + | 'immutable' | 'maxSignals' | 'savedId' | 'riskScore' @@ -99,11 +100,25 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; + immutable: boolean; }; export type ImportRuleAlertRest = Omit & { id: string | undefined | null; rule_id: string; + immutable: boolean; }; +export type PrepackagedRules = Omit< + RuleAlertParamsRest, + | 'status' + | 'status_date' + | 'last_failure_at' + | 'last_success_at' + | 'last_failure_message' + | 'last_success_message' + | 'updated_at' + | 'created_at' +> & { rule_id: string; immutable: boolean }; + export type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index e2f92e0acd6456..53627d1cf2f6b3 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -138,6 +138,7 @@ export const getColumns = ( name: 'ID', sortable: true, truncateText: true, + scope: 'row', }, { field: TRANSFORM_LIST_COLUMN.DESCRIPTION, diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index e090a2c85e1366..cf7332f97d466b 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -9,6 +9,7 @@ import { resolve } from 'path'; import { PluginInitializerContext } from 'src/core/server'; import { PLUGIN } from './common/constants'; import { KibanaServer, plugin } from './server'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export const uptime = (kibana: any) => new kibana.Plugin({ @@ -30,6 +31,7 @@ export const uptime = (kibana: any) => main: 'plugins/uptime/app', order: 8900, url: '/app/uptime#/', + category: DEFAULT_APP_CATEGORIES.observability, }, home: ['plugins/uptime/register_feature'], }, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index 731f560d315d63..679106f7e19b4d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -13,10 +13,10 @@ import { Typeahead } from './typeahead'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; import { - AutocompleteProviderRegister, - AutocompleteSuggestion, esKuery, IIndexPattern, + autocomplete, + DataPublicPluginStart, } from '../../../../../../../../src/plugins/data/public'; import { useIndexPattern } from '../../../hooks'; @@ -25,7 +25,7 @@ const Container = styled.div` `; interface State { - suggestions: AutocompleteSuggestion[]; + suggestions: autocomplete.QuerySuggestion[]; isLoadingIndexPattern: boolean; } @@ -34,38 +34,11 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { return esKuery.toElasticsearchQuery(ast, indexPattern); } -function getSuggestions( - query: string, - selectionStart: number, - apmIndexPattern: IIndexPattern, - autocomplete: Pick -) { - const autocompleteProvider = autocomplete.getProvider('kuery'); - if (!autocompleteProvider) { - return []; - } - const config = { - get: () => true, - }; - - const getAutocompleteSuggestions = autocompleteProvider({ - config, - indexPatterns: [apmIndexPattern], - }); - - const suggestions = getAutocompleteSuggestions({ - query, - selectionStart, - selectionEnd: selectionStart, - }); - return suggestions; -} - interface Props { - autocomplete: Pick; + autocomplete: DataPublicPluginStart['autocomplete']; } -export function KueryBar({ autocomplete }: Props) { +export function KueryBar({ autocomplete: autocompleteService }: Props) { const [state, setState] = useState({ suggestions: [], isLoadingIndexPattern: true, @@ -99,14 +72,16 @@ export function KueryBar({ autocomplete }: Props) { currentRequestCheck = currentRequest; try { - let suggestions = await getSuggestions( - inputValue, - selectionStart, - indexPattern, - autocomplete - ); - suggestions = suggestions - .filter((suggestion: AutocompleteSuggestion) => !startsWith(suggestion.text, 'span.')) + const suggestions = ( + (await autocompleteService.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + query: inputValue, + selectionStart, + selectionEnd: selectionStart, + })) || [] + ) + .filter(suggestion => !startsWith(suggestion.text, 'span.')) .slice(0, 15); if (currentRequest !== currentRequestCheck) { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx index 98780d23c5a629..11f6565734782e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -12,6 +12,7 @@ import { start } from '../../../../../../../../../src/legacy/core_plugins/embedd import * as i18n from './translations'; // @ts-ignore import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; +import { Location } from '../../../../../common/runtime_types'; import { MapEmbeddable } from './types'; import { getLayerList } from './map_config'; @@ -22,10 +23,7 @@ export interface EmbeddedMapProps { downPoints: LocationPoint[]; } -export interface LocationPoint { - lat: string; - lon: string; -} +export type LocationPoint = Required; const EmbeddedPanel = styled.div` z-index: auto; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts index d4601baefdf30c..a43edae4382527 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts @@ -5,7 +5,7 @@ */ import lowPolyLayerFeatures from './low_poly_layer.json'; -import { LocationPoint } from './embedded_map'; +import { LocationPoint } from './embedded_map.js'; import { UptimeAppColors } from '../../../../uptime_app'; /** @@ -16,7 +16,7 @@ import { UptimeAppColors } from '../../../../uptime_app'; export const getLayerList = ( upPoints: LocationPoint[], downPoints: LocationPoint[], - { gray, danger }: Pick + { danger }: Pick ) => { return [getLowPolyLayer(), getDownPointsLayer(downPoints, danger), getUpPointsLayer(upPoints)]; }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx index d35e1281260e2e..c93e16d0a080b5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiErrorBoundary } from '@elastic/eui'; import { LocationStatusTags } from './location_status_tags'; import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; -import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorLocations, MonitorLocation } from '../../../../common/runtime_types'; import { UNNAMED_LOCATION } from '../../../../common/constants'; import { LocationMissingWarning } from './location_missing'; @@ -32,15 +32,23 @@ export const LocationMap = ({ monitorLocations }: LocationMapProps) => { let isGeoInfoMissing = false; if (monitorLocations?.locations) { - monitorLocations.locations.forEach((item: any) => { - if (item.geo?.name !== UNNAMED_LOCATION) { - if (item.summary.down === 0) { - upPoints.push(item.geo.location); + monitorLocations.locations.forEach((item: MonitorLocation) => { + if (item.geo?.name === UNNAMED_LOCATION || !item.geo?.location) { + isGeoInfoMissing = true; + } else if ( + item.geo?.name !== UNNAMED_LOCATION && + !!item.geo.location.lat && + !!item.geo.location.lon + ) { + // TypeScript doesn't infer that the above checks in this block's condition + // ensure that lat and lon are defined when we try to pass the location object directly, + // but if we destructure the values it does. Improvement to this block is welcome. + const { lat, lon } = item.geo.location; + if (item?.summary?.down === 0) { + upPoints.push({ lat, lon }); } else { - downPoints.push(item.geo.location); + downPoints.push({ lat, lon }); } - } else if (item.geo?.name === UNNAMED_LOCATION) { - isGeoInfoMissing = true; } }); } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx index 923bf2c68cc56a..e2855f78262e75 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_status_tags.tsx @@ -66,44 +66,57 @@ export const LocationStatusTags = ({ locations }: Props) => { return a.label > b.label ? 1 : b.label > a.label ? -1 : 0; }); - moment.updateLocale('en', { - relativeTime: { - future: 'in %s', - past: '%s ago', - s: '%ds', - ss: '%ss', - m: '%dm', - mm: '%dm', - h: '%dh', - hh: '%dh', - d: '%dd', - dd: '%dd', - M: '%d Mon', - MM: '%d Mon', - y: '%d Yr', - yy: '%d Yr', - }, - }); + const tagLabel = (item: StatusTag, ind: number, color: string) => { + return ( + + + + {item.label} + + + + {moment(item.timestamp).fromNow()} + + + ); + }; - const tagLabel = (item: StatusTag, ind: number, color: string) => ( - - - - {item.label} - - - - {moment(item.timestamp).fromNow()} - - - ); + const prevLocal: string = moment.locale() ?? 'en'; - return ( - <> + const renderTags = () => { + moment.defineLocale('en-tag', { + relativeTime: { + future: 'in %s', + past: '%s ago', + s: '%ds', + ss: '%ss', + m: '%dm', + mm: '%dm', + h: '%dh', + hh: '%dh', + d: '%dd', + dd: '%dd', + M: '%d Mon', + MM: '%d Mon', + y: '%d Yr', + yy: '%d Yr', + }, + }); + const tags = ( {downLocations.map((item, ind) => tagLabel(item, ind, danger))} {upLocations.map((item, ind) => tagLabel(item, ind, gray))} + ); + + // Need to reset locale so it doesn't effect other parts of the app + moment.locale(prevLocal); + return tags; + }; + + return ( + <> + {renderTags()} {locations.length > 7 && ( diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx index b529d5346e88ea..4b3abb46ac1e4a 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx +++ b/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx @@ -37,7 +37,7 @@ const defaultContext: UptimeSettingsContextValues = { export const UptimeSettingsContext = createContext(defaultContext); export const UptimeSettingsContextProvider: React.FC = ({ children, ...props }) => { - const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable } = props; + const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, commonlyUsedRanges } = props; const { dateRangeStart, dateRangeEnd } = useParams(); @@ -47,10 +47,19 @@ export const UptimeSettingsContextProvider: React.FC = ({ childr isApmAvailable, isInfraAvailable, isLogsAvailable, + commonlyUsedRanges, dateRangeStart: dateRangeStart ?? DATE_RANGE_START, dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END, }; - }, [basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, dateRangeStart, dateRangeEnd]); + }, [ + basePath, + isApmAvailable, + isInfraAvailable, + isLogsAvailable, + dateRangeStart, + dateRangeEnd, + commonlyUsedRanges, + ]); return ; }; diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index fbfbfc06e3c52f..1c14d971120be0 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -20,14 +20,14 @@ import { useIndexPattern, useUrlParams, useUptimeTelemetry, UptimePage } from '. import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; -import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; import { store } from '../state'; import { setEsKueryString } from '../state/actions'; import { PageHeader } from './page_header'; +import { esKuery, DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { UptimeThemeContext } from '../contexts/uptime_theme_context'; interface OverviewPageProps { - autocomplete: Pick; + autocomplete: DataPublicPluginStart['autocomplete']; setBreadcrumbs: UMUpdateBreadcrumbs; } diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index 08d752f5b32abe..028f2d5958325a 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -7,14 +7,14 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import { MonitorPage, OverviewPage, NotFoundPage } from './pages'; -import { AutocompleteProviderRegister } from '../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { UMUpdateBreadcrumbs } from './lib/lib'; export const MONITOR_ROUTE = '/monitor/:monitorId/:location?'; export const OVERVIEW_ROUTE = '/'; interface RouterProps { - autocomplete: Pick; + autocomplete: DataPublicPluginStart['autocomplete']; basePath: string; setBreadcrumbs: UMUpdateBreadcrumbs; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index c86e0db9ae04ad..e433931f03c8e6 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -346,16 +346,18 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = { const result = await callES('search', params); const locations = result?.aggregations?.location?.buckets ?? []; - const getGeo = (locGeo: any) => { + const getGeo = (locGeo: { name: string; location?: string }) => { if (locGeo) { const { name, location } = locGeo; - const latLon = location.trim().split(','); + const latLon = location?.trim().split(','); return { name, - location: { - lat: latLon[0], - lon: latLon[1], - }, + location: latLon + ? { + lat: latLon[0], + lon: latLon[1], + } + : undefined, }; } else { return { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index d69e068bdea3ad..7598141bdea659 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -19,12 +19,13 @@ import { appStoreFactory } from './store'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - const store = appStoreFactory(coreStart); + const [store, stopSagas] = appStoreFactory(coreStart); ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); + stopSagas(); }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts index 0387eac0e7c7fa..91841f75c24fec 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.test.ts @@ -3,18 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { createSagaMiddleware, SagaContext } from './index'; -import { applyMiddleware, createStore, Reducer } from 'redux'; + +import { createSagaMiddleware, SagaContext, SagaMiddleware } from './index'; +import { applyMiddleware, createStore, Reducer, Store } from 'redux'; describe('saga', () => { const INCREMENT_COUNTER = 'INCREMENT'; const DELAYED_INCREMENT_COUNTER = 'DELAYED INCREMENT COUNTER'; const STOP_SAGA_PROCESSING = 'BREAK ASYNC ITERATOR'; - const sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)); + const sleep = (ms = 10) => new Promise(resolve => setTimeout(resolve, ms)); + let store: Store; let reducerA: Reducer; let sideAffect: (a: unknown, s: unknown) => void; let sagaExe: (sagaContext: SagaContext) => Promise; + let sagaExeReduxMiddleware: SagaMiddleware; beforeEach(() => { reducerA = jest.fn((prevState = { count: 0 }, { type }) => { @@ -47,53 +50,63 @@ describe('saga', () => { } } }); + + sagaExeReduxMiddleware = createSagaMiddleware(sagaExe); + store = createStore(reducerA, applyMiddleware(sagaExeReduxMiddleware)); }); - test('it returns Redux Middleware from createSagaMiddleware()', () => { - const sagaMiddleware = createSagaMiddleware(async () => {}); - expect(sagaMiddleware).toBeInstanceOf(Function); + afterEach(() => { + sagaExeReduxMiddleware.stop(); }); + test('it does nothing if saga is not started', () => { - const store = createStore(reducerA, applyMiddleware(createSagaMiddleware(sagaExe))); - expect(store.getState().count).toEqual(0); - expect(reducerA).toHaveBeenCalled(); - expect(sagaExe).toHaveBeenCalled(); - expect(sideAffect).not.toHaveBeenCalled(); - expect(store.getState()).toEqual({ count: 0 }); + expect(sagaExe).not.toHaveBeenCalled(); }); - test('it updates store once running', async () => { - const sagaMiddleware = createSagaMiddleware(sagaExe); - const store = createStore(reducerA, applyMiddleware(sagaMiddleware)); + test('it can dispatch store actions once running', async () => { + sagaExeReduxMiddleware.start(); expect(store.getState()).toEqual({ count: 0 }); expect(sagaExe).toHaveBeenCalled(); store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); expect(store.getState()).toEqual({ count: 0 }); - await sleep(100); + await sleep(); expect(sideAffect).toHaveBeenCalled(); expect(store.getState()).toEqual({ count: 1 }); }); - test('it stops processing if break out of loop', async () => { - const sagaMiddleware = createSagaMiddleware(sagaExe); - const store = createStore(reducerA, applyMiddleware(sagaMiddleware)); + test('it stops processing if break out of loop', async () => { + sagaExeReduxMiddleware.start(); store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(100); + await sleep(); expect(store.getState()).toEqual({ count: 1 }); expect(sideAffect).toHaveBeenCalledTimes(2); store.dispatch({ type: STOP_SAGA_PROCESSING }); - await sleep(100); + await sleep(); + + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + await sleep(); + + expect(store.getState()).toEqual({ count: 1 }); + expect(sideAffect).toHaveBeenCalledTimes(2); + }); + + test('it stops saga middleware when stop() is called', async () => { + sagaExeReduxMiddleware.start(); + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); + await sleep(); expect(store.getState()).toEqual({ count: 1 }); expect(sideAffect).toHaveBeenCalledTimes(2); + sagaExeReduxMiddleware.stop(); + store.dispatch({ type: DELAYED_INCREMENT_COUNTER }); - await sleep(100); + await sleep(); expect(store.getState()).toEqual({ count: 1 }); expect(sideAffect).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts index b93360ec6b5aa7..bca6aa6563fe52 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/lib/saga.ts @@ -35,7 +35,20 @@ export interface SagaContext { dispatch: Dispatch; } +export interface SagaMiddleware extends Middleware { + /** + * Start the saga. Should be called after the `store` has been created + */ + start: () => void; + + /** + * Stop the saga by exiting the internal generator `for await...of` loop. + */ + stop: () => void; +} + const noop = () => {}; +const STOP = Symbol('STOP'); /** * Creates Saga Middleware for use with Redux. @@ -43,7 +56,7 @@ const noop = () => {}; * @param {Saga} saga The `saga` should initialize a long-running `for await...of` loop against * the return value of the `actionsAndState()` method provided by the `SagaContext`. * - * @return {Middleware} + * @return {SagaMiddleware} * * @example * @@ -64,22 +77,31 @@ const noop = () => {}; * //.... * const store = createStore(reducers, [ endpointsSagaMiddleware ]); */ -export function createSagaMiddleware(saga: Saga): Middleware { +export function createSagaMiddleware(saga: Saga): SagaMiddleware { const iteratorInstances = new Set(); let runSaga: () => void = noop; + let stopSaga: () => void = noop; + let runningPromise: Promise; async function* getActionsAndStateIterator(): StoreActionsAndState { const instance: IteratorInstance = { queue: [], nextResolve: null }; iteratorInstances.add(instance); + try { while (true) { - yield await nextActionAndState(); + const actionAndState = await Promise.race([nextActionAndState(), runningPromise]); + + if (actionAndState === STOP) { + break; + } + + yield actionAndState as QueuedAction; } } finally { // If the consumer stops consuming this (e.g. `break` or `return` is called in the `for await` // then this `finally` block will run and unregister this instance and reset `runSaga` iteratorInstances.delete(instance); - runSaga = noop; + runSaga = stopSaga = noop; } function nextActionAndState() { @@ -109,7 +131,6 @@ export function createSagaMiddleware(saga: Saga): Middleware { actionsAndState: getActionsAndStateIterator, dispatch, }); - runSaga(); } return (next: Dispatch) => (action: AnyAction) => { // Call the next dispatch method in the middleware chain. @@ -125,5 +146,14 @@ export function createSagaMiddleware(saga: Saga): Middleware { }; } + middleware.start = () => { + runningPromise = new Promise(resolve => (stopSaga = () => resolve(STOP))); + runSaga(); + }; + + middleware.stop = () => { + stopSaga(); + }; + return middleware; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts index 92bf3b7fd92dd8..6bf946873e1797 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts @@ -24,6 +24,7 @@ describe('endpoint list saga', () => { let fakeHttpServices: jest.Mocked; let store: Store; let dispatch: Dispatch; + let stopSagas: () => void; // TODO: consolidate the below ++ helpers in `index.test.ts` into a `test_helpers.ts`?? const generateEndpoint = (): EndpointData => { @@ -89,13 +90,19 @@ describe('endpoint list saga', () => { beforeEach(() => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); fakeHttpServices = fakeCoreStart.http as jest.Mocked; - store = createStore( - endpointListReducer, - applyMiddleware(createSagaMiddleware(endpointListSagaFactory())) - ); + + const sagaMiddleware = createSagaMiddleware(endpointListSagaFactory()); + store = createStore(endpointListReducer, applyMiddleware(sagaMiddleware)); + + sagaMiddleware.start(); + stopSagas = sagaMiddleware.stop; dispatch = store.dispatch; }); + afterEach(() => { + stopSagas(); + }); + test('it handles `userEnteredEndpointListPage`', async () => { const apiResponse = getEndpointListApiResponse(); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index d0dc002031ce2f..bfa1385b9f0ac0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, compose, applyMiddleware } from 'redux'; +import { createStore, compose, applyMiddleware, Store } from 'redux'; import { CoreStart } from 'kibana/public'; import { appSagaFactory } from './saga'; import { appReducer } from './reducer'; @@ -15,10 +15,13 @@ const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMP ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' }) : compose; -export const appStoreFactory = (coreStart: CoreStart) => { +export const appStoreFactory = (coreStart: CoreStart): [Store, () => void] => { + const sagaReduxMiddleware = appSagaFactory(coreStart); const store = createStore( appReducer, - composeWithReduxDevTools(applyMiddleware(appSagaFactory(coreStart))) + composeWithReduxDevTools(applyMiddleware(sagaReduxMiddleware)) ); - return store; + + sagaReduxMiddleware.start(); + return [store, sagaReduxMiddleware.stop]; }; diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 87d373d3a4f34c..7dd878d579043b 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; + import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; - let mockCoreSetup: MockedKeys; + let mockCoreSetup: ReturnType; let mockedEndpointPluginSetupDependencies: jest.Mocked; let mockedPluginSetupContract: jest.Mocked; beforeEach(() => { diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index 9547a2dc529665..c9fbb61e6cc19b 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -261,7 +261,6 @@ describe('licensing plugin', () => { expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); await flushPromises(customPollingFrequency * 1.5); - expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(2); expect(customLicense.isAvailable).toBe(true); expect(customLicense.type).toBe('gold'); diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index f3c65ed7e3cf17..121791d113bd5d 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -10,7 +10,16 @@ export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { BuiltinESPrivileges } from './builtin_es_privileges'; export { FeaturesPrivileges } from './features_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; -export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { + Role, + RoleIndexPrivilege, + RoleKibanaPrivilege, + copyRole, + isReadOnlyRole, + isReservedRole, + isRoleEnabled, + prepareRoleClone, +} from './role'; export { KibanaPrivileges } from './kibana_privileges'; export { InlineRoleTemplate, diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts b/x-pack/plugins/security/common/model/role.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/lib/role_utils.test.ts rename to x-pack/plugins/security/common/model/role.test.ts index 9d94017c3f0fe4..d4a910a1785ebd 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts +++ b/x-pack/plugins/security/common/model/role.test.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { - copyRole, - isReadOnlyRole, - isReservedRole, - isRoleEnabled, - prepareRoleClone, -} from './role_utils'; +import { Role, isReadOnlyRole, isReservedRole, isRoleEnabled, copyRole, prepareRoleClone } from '.'; describe('role', () => { describe('isRoleEnabled', () => { diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 89f68aaa55b5cb..1edcf147262ede 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { @@ -40,3 +41,53 @@ export interface Role { _transform_error?: string[]; _unrecognized_applications?: string[]; } + +/** + * Returns whether given role is enabled or not + * + * @param role Object Role JSON, as returned by roles API + * @return Boolean true if role is enabled; false otherwise + */ +export function isRoleEnabled(role: Partial) { + return role.transient_metadata?.enabled ?? true; +} + +/** + * Returns whether given role is reserved or not. + * + * @param role Role as returned by roles API + */ +export function isReservedRole(role: Partial) { + return (role.metadata?._reserved as boolean) ?? false; +} + +/** + * Returns whether given role is editable through the UI or not. + * + * @param role the Role as returned by roles API + */ +export function isReadOnlyRole(role: Partial): boolean { + return isReservedRole(role) || (role._transform_error?.length ?? 0) > 0; +} + +/** + * Returns a deep copy of the role. + * + * @param role the Role to copy. + */ +export function copyRole(role: Role) { + return cloneDeep(role); +} + +/** + * Creates a deep copy of the role suitable for cloning. + * + * @param role the Role to clone. + */ +export function prepareRoleClone(role: Role): Role { + const clone = copyRole(role); + + clone.name = ''; + + return clone; +} diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 32f860b1423d30..7d1940e393becf 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -3,7 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], - "requiredPlugins": ["features", "licensing"], + "requiredPlugins": ["data", "features", "licensing"], + "optionalPlugins": ["home", "management"], "server": true, "ui": true } diff --git a/x-pack/plugins/security/public/_index.scss b/x-pack/plugins/security/public/_index.scss new file mode 100644 index 00000000000000..9fa81bad7c3f40 --- /dev/null +++ b/x-pack/plugins/security/public/_index.scss @@ -0,0 +1,2 @@ +// Management styles +@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.test.tsx index 366842e58e9e4a..b7cf8e6dd14181 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -6,11 +6,12 @@ import React from 'react'; import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { securityMock } from '../../../../../../../plugins/security/public/mocks'; +import { AuthenticatedUser } from '../../common/model'; import { AccountManagementPage } from './account_management_page'; -import { AuthenticatedUser } from '../../../../common/model'; -jest.mock('ui/kfetch'); +import { coreMock } from 'src/core/public/mocks'; +import { securityMock } from '../mocks'; +import { userAPIClientMock } from '../management/users/index.mock'; interface Options { withFullName?: boolean; @@ -45,7 +46,11 @@ describe('', () => { it(`displays users full name, username, and email address`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -63,7 +68,11 @@ describe('', () => { it(`displays username when full_name is not provided`, async () => { const user = createUser({ withFullName: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -77,7 +86,11 @@ describe('', () => { it(`displays a placeholder when no email address is provided`, async () => { const user = createUser({ withEmail: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -91,7 +104,11 @@ describe('', () => { it(`displays change password form for users in the native realm`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -106,7 +123,11 @@ describe('', () => { it(`does not display change password form for users in the saml realm`, async () => { const user = createUser({ realm: 'saml' }); const wrapper = mountWithIntl( - + ); await act(async () => { diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx similarity index 62% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.tsx index 6abee73e0b3535..3f764adc7949f2 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -5,20 +5,24 @@ */ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/public'; -import { getUserDisplayName, AuthenticatedUser } from '../../../../common/model'; +import { NotificationsStart } from 'src/core/public'; +import { getUserDisplayName, AuthenticatedUser } from '../../common/model'; +import { AuthenticationServiceSetup } from '../authentication'; import { ChangePassword } from './change_password'; +import { UserAPIClient } from '../management'; import { PersonalInfo } from './personal_info'; interface Props { - securitySetup: SecurityPluginSetup; + authc: AuthenticationServiceSetup; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } -export const AccountManagementPage = (props: Props) => { +export const AccountManagementPage = ({ apiClient, authc, notifications }: Props) => { const [currentUser, setCurrentUser] = useState(null); useEffect(() => { - props.securitySetup.authc.getCurrentUser().then(setCurrentUser); - }, [props]); + authc.getCurrentUser().then(setCurrentUser); + }, [authc]); if (!currentUser) { return null; @@ -36,7 +40,7 @@ export const AccountManagementPage = (props: Props) => { - + diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx similarity index 81% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx rename to x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 63abb4539470d2..f5ac5f3b21d2e6 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -3,18 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { UserAPIClient } from '../../../../lib/api'; -import { AuthenticatedUser, canUserChangePassword } from '../../../../../common/model'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { EuiDescribedFormGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsSetup } from 'src/core/public'; +import { AuthenticatedUser, canUserChangePassword } from '../../../common/model'; +import { UserAPIClient } from '../../management/users'; +import { ChangePasswordForm } from '../../management/users/components/change_password_form'; interface Props { user: AuthenticatedUser; + apiClient: PublicMethodsOf; + notifications: NotificationsSetup; } export class ChangePassword extends Component { @@ -48,7 +48,8 @@ export class ChangePassword extends Component { ); diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts b/x-pack/plugins/security/public/account_management/change_password/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts rename to x-pack/plugins/security/public/account_management/change_password/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/index.ts b/x-pack/plugins/security/public/account_management/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/index.ts rename to x-pack/plugins/security/public/account_management/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts b/x-pack/plugins/security/public/account_management/personal_info/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts rename to x-pack/plugins/security/public/account_management/personal_info/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx rename to x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx index 7121bf7ab28ee7..9cbbc242e8400e 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx +++ b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx @@ -3,15 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, - EuiFormRow, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { AuthenticatedUser } from '../../../../../common/model'; +import { EuiDescribedFormGroup, EuiFormRow, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AuthenticatedUser } from '../../../common/model'; interface Props { user: AuthenticatedUser; diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss new file mode 100644 index 00000000000000..5d419b53230799 --- /dev/null +++ b/x-pack/plugins/security/public/management/_index.scss @@ -0,0 +1,3 @@ +@import './roles/index'; +@import './users/index'; +@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts new file mode 100644 index 00000000000000..2a45d497029f41 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.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. + */ + +export const apiKeysAPIClientMock = { + create: () => ({ + checkPrivileges: jest.fn(), + getApiKeys: jest.fn(), + invalidateApiKeys: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts new file mode 100644 index 00000000000000..7d51a80459a6e9 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APIKeysAPIClient } from './api_keys_api_client'; + +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('APIKeysAPIClient', () => { + it('checkPrivileges() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.checkPrivileges()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key/privileges'); + }); + + it('getApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.getApiKeys()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(false)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(true)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: true }, + }); + }); + + it('invalidateApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.post.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + const mockAPIKeys = [ + { id: 'one', name: 'name-one' }, + { id: 'two', name: 'name-two' }, + ]; + + await expect(apiClient.invalidateApiKeys(mockAPIKeys)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, false)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, true)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: true }), + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts new file mode 100644 index 00000000000000..372b1e56a73c47 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; + +interface CheckPrivilegesResponse { + areApiKeysEnabled: boolean; + isAdmin: boolean; +} + +interface InvalidateApiKeysResponse { + itemsInvalidated: ApiKeyToInvalidate[]; + errors: any[]; +} + +interface GetApiKeysResponse { + apiKeys: ApiKey[]; +} + +const apiKeysUrl = '/internal/security/api_key'; + +export class APIKeysAPIClient { + constructor(private readonly http: HttpStart) {} + + public async checkPrivileges() { + return await this.http.get(`${apiKeysUrl}/privileges`); + } + + public async getApiKeys(isAdmin = false) { + return await this.http.get(apiKeysUrl, { query: { isAdmin } }); + } + + public async invalidateApiKeys(apiKeys: ApiKeyToInvalidate[], isAdmin = false) { + return await this.http.post(`${apiKeysUrl}/invalidate`, { + body: JSON.stringify({ apiKeys, isAdmin }), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index c2537235c99f61..42fd4417e238b1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ApiKeysGridPage renders a callout when API keys are not enabled 1`] = ` - +exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` + Contact your system administrator and refer to the `; -exports[`ApiKeysGridPage renders permission denied if user does not have required permissions 1`] = ` +exports[`APIKeysGridPage renders permission denied if user does not have required permissions 1`] = ` ({ body: { statusCode: 403 } }); -const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); - -jest.mock('../../../../lib/api_keys_api', () => { - return { - ApiKeysApi: { - async checkPrivileges() { - if (mockSimulate403) { - throw mock403(); - } - - return { - isAdmin: mockIsAdmin, - areApiKeysEnabled: mockAreApiKeysEnabled, - }; - }, - async getApiKeys() { - if (mockSimulate500) { - throw mock500(); - } - - return { - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'my-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], - }; - }, - }, - }; -}); - import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ApiKeysGridPage } from './api_keys_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; import { NotEnabled } from './not_enabled'; import { PermissionDenied } from './permission_denied'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; +import { APIKeysGridPage } from './api_keys_grid_page'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { apiKeysAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); +const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( wrapper: ReactWrapper, @@ -77,23 +41,51 @@ const waitForRender = async ( }); }; -describe('ApiKeysGridPage', () => { +describe('APIKeysGridPage', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; - mockSimulate500 = false; - mockAreApiKeysEnabled = true; - mockIsAdmin = true; + apiClientMock = apiKeysAPIClientMock.create(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: true, + }); + apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'my-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }); }); + const getViewProperties = () => { + const { docLinks, notifications } = coreMock.createStart(); + return { + docLinks: new DocumentationLinksService(docLinks), + notifications, + apiKeysAPIClient: apiClientMock, + }; + }; + it('renders a loading state when fetching API keys', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); }); it('renders a callout when API keys are not enabled', async () => { - mockAreApiKeysEnabled = false; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: false, + }); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(NotEnabled).length > 0; @@ -103,8 +95,9 @@ describe('ApiKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; @@ -114,8 +107,9 @@ describe('ApiKeysGridPage', () => { }); it('renders error callout if error fetching API keys', async () => { - mockSimulate500 = true; - const wrapper = mountWithIntl(); + apiClientMock.getApiKeys.mockRejectedValue(mock500()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(EuiCallOut).length > 0; @@ -125,7 +119,10 @@ describe('ApiKeysGridPage', () => { }); describe('Admin view', () => { - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mountWithIntl(); + }); it('renders a callout indicating the user is an administrator', async () => { const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; @@ -151,8 +148,15 @@ describe('ApiKeysGridPage', () => { }); describe('Non-admin view', () => { - mockIsAdmin = false; - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: false, + areApiKeysEnabled: true, + }); + + wrapper = mountWithIntl(); + }); it('does NOT render a callout indicating the user is an administrator', async () => { const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 92633a4b0ef57c..779a2302cfadf9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -27,16 +27,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; +import { NotificationsStart } from 'src/core/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; -import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model'; -import { ApiKeysApi } from '../../../../lib/api_keys_api'; +import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; +import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; import { NotEnabled } from './not_enabled'; import { InvalidateProvider } from './invalidate_provider'; +interface Props { + notifications: NotificationsStart; + docLinks: DocumentationLinksService; + apiKeysAPIClient: PublicMethodsOf; +} + interface State { isLoadingApp: boolean; isLoadingTable: boolean; @@ -50,7 +57,7 @@ interface State { const DATE_FORMAT = 'MMMM Do YYYY HH:mm:ss'; -export class ApiKeysGridPage extends Component { +export class APIKeysGridPage extends Component { constructor(props: any) { super(props); this.state = { @@ -124,7 +131,7 @@ export class ApiKeysGridPage extends Component { if (!areApiKeysEnabled) { return ( - + ); } @@ -132,7 +139,7 @@ export class ApiKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + ); } @@ -210,7 +217,11 @@ export class ApiKeysGridPage extends Component { const search: EuiInMemoryTableProps['search'] = { toolsLeft: selectedItems.length ? ( - + {invalidateApiKeyPrompt => { return ( { return ( - + {invalidateApiKeyPrompt => { return ( { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await ApiKeysApi.checkPrivileges(); + const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); this.setState({ isAdmin, areApiKeysEnabled }); if (areApiKeysEnabled) { @@ -494,14 +509,11 @@ export class ApiKeysGridPage extends Component { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - toastNotifications.addDanger( - this.props.i18n.translate( - 'xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', - { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -520,7 +532,7 @@ export class ApiKeysGridPage extends Component { private loadApiKeys = async () => { try { const { isAdmin } = this.state; - const { apiKeys } = await ApiKeysApi.getApiKeys(isAdmin); + const { apiKeys } = await this.props.apiKeysAPIClient.getApiKeys(isAdmin); this.setState({ apiKeys }); } catch (e) { this.setState({ error: e }); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx index 957ca7010a1a0a..7d762a1ceb04e0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx @@ -7,13 +7,14 @@ import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { isAdmin: boolean; + docLinks: DocumentationLinksService; } -export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => ( +export const EmptyPrompt: React.FunctionComponent = ({ isAdmin, docLinks }) => ( = ({ isAdmin }) => ( defaultMessage="You can create an {link} from Console." values={{ link: ( - + React.ReactElement; + notifications: NotificationsStart; + apiKeysAPIClient: PublicMethodsOf; } export type InvalidateApiKeys = ( @@ -23,7 +25,12 @@ export type InvalidateApiKeys = ( type OnSuccessCallback = (apiKeysInvalidated: ApiKeyToInvalidate[]) => void; -export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, children }) => { +export const InvalidateProvider: React.FunctionComponent = ({ + isAdmin, + children, + notifications, + apiKeysAPIClient, +}) => { const [apiKeys, setApiKeys] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const onSuccessCallback = useRef(null); @@ -48,7 +55,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch let errors; try { - result = await ApiKeysApi.invalidateApiKeys(apiKeys, isAdmin); + result = await apiKeysAPIClient.invalidateApiKeys(apiKeys, isAdmin); } catch (e) { error = e; } @@ -77,7 +84,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: itemsInvalidated[0].name }, } ); - toastNotifications.addSuccess(successMessage); + notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { onSuccessCallback.current([...itemsInvalidated]); } @@ -106,7 +113,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: (errors && errors[0].name) || apiKeys[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx index c419e15450c1e1..08fe5425577573 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NotEnabled: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NotEnabled: React.FunctionComponent = ({ docLinks }) => ( ( defaultMessage="Contact your system administrator and refer to the {link} to enable API keys." values={{ link: ( - + ({ + APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, +})); + +import { apiKeysManagementApp } from './api_keys_management_app'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +describe('apiKeysManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { getStartServices } = coreMock.createSetup(); + + expect(apiKeysManagementApp.create({ getStartServices: getStartServices as any })) + .toMatchInlineSnapshot(` + Object { + "id": "api_keys", + "mount": [Function], + "order": 30, + "title": "API Keys", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const { getStartServices } = coreMock.createSetup(); + const container = document.createElement('div'); + + const setBreadcrumbs = jest.fn(); + const unmount = await apiKeysManagementApp + .create({ getStartServices: getStartServices as any }) + .mount({ + basePath: '/some-base-path', + element: container, + setBreadcrumbs, + }); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '#/some-base-path', text: 'API Keys' }]); + expect(container).toMatchInlineSnapshot(` +
+ Page: {"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"apiKeysAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx new file mode 100644 index 00000000000000..35de732b84ce9b --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { APIKeysGridPage } from './api_keys_grid'; +import { APIKeysAPIClient } from './api_keys_api_client'; +import { DocumentationLinksService } from './documentation_links'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const apiKeysManagementApp = Object.freeze({ + id: 'api_keys', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 30, + title: i18n.translate('xpack.security.management.apiKeysTitle', { + defaultMessage: 'API Keys', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + setBreadcrumbs([ + { + text: i18n.translate('xpack.security.apiKeys.breadcrumb', { + defaultMessage: 'API Keys', + }), + href: `#${basePath}`, + }, + ]); + + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts similarity index 51% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts rename to x-pack/plugins/security/public/management/api_keys/documentation_links.ts index 1f03763eb542a3..4165c2a2372c96 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts +++ b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { DocLinksStart } from 'src/core/public'; -class DocumentationLinksService { - private esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; +export class DocumentationLinksService { + private readonly esDocBasePath: string; - public getApiKeyServiceSettingsDocUrl(): string { + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getApiKeyServiceSettingsDocUrl() { return `${this.esDocBasePath}security-settings.html#api-key-service-settings`; } - public getCreateApiKeyDocUrl(): string { + public getCreateApiKeyDocUrl() { return `${this.esDocBasePath}security-api-create-api-key.html`; } } - -export const documentationLinks = new DocumentationLinksService(); diff --git a/x-pack/plugins/security/public/management/api_keys/index.mock.ts b/x-pack/plugins/security/public/management/api_keys/index.mock.ts new file mode 100644 index 00000000000000..3c11cd6bb9c654 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/index.mock.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { apiKeysAPIClientMock } from './api_keys_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/api_keys/index.ts b/x-pack/plugins/security/public/management/api_keys/index.ts new file mode 100644 index 00000000000000..e15da7d5eb4092 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { apiKeysManagementApp } from './api_keys_management_app'; diff --git a/x-pack/plugins/security/public/management/index.ts b/x-pack/plugins/security/public/management/index.ts new file mode 100644 index 00000000000000..e1a13d66e68836 --- /dev/null +++ b/x-pack/plugins/security/public/management/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ManagementService } from './management_service'; +export { UserAPIClient } from './users/user_api_client'; diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts new file mode 100644 index 00000000000000..53c12ad7ab12c4 --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { BehaviorSubject } from 'rxjs'; +import { ManagementApp } from '../../../../../src/plugins/management/public'; +import { SecurityLicenseFeatures } from '../../common/licensing/license_features'; +import { ManagementService } from './management_service'; +import { usersManagementApp } from './users'; + +import { coreMock } from '../../../../../src/core/public/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { rolesManagementApp } from './roles'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; + +describe('ManagementService', () => { + describe('setup()', () => { + it('properly registers security section and its applications', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + const { authc } = securityMock.createSetup(); + const license = licenseMock.create(); + + const mockSection = { registerApp: jest.fn() }; + const managementSetup = { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue(mockSection), + }, + }; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc, + management: managementSetup, + }); + + expect(managementSetup.sections.register).toHaveBeenCalledTimes(1); + expect(managementSetup.sections.register).toHaveBeenCalledWith({ + id: 'security', + title: 'Security', + order: 100, + euiIconType: 'securityApp', + }); + + expect(mockSection.registerApp).toHaveBeenCalledTimes(4); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'users', + mount: expect.any(Function), + order: 10, + title: 'Users', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'roles', + mount: expect.any(Function), + order: 20, + title: 'Roles', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'api_keys', + mount: expect.any(Function), + order: 30, + title: 'API Keys', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'role_mappings', + mount: expect.any(Function), + order: 40, + title: 'Role Mappings', + }); + }); + }); + + describe('start()', () => { + function startService(initialFeatures: Partial) { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + const licenseSubject = new BehaviorSubject( + (initialFeatures as unknown) as SecurityLicenseFeatures + ); + const license = licenseMock.create(); + license.features$ = licenseSubject; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc: securityMock.createSetup().authc, + management: { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue({ registerApp: jest.fn() }), + }, + }, + }); + + const getMockedApp = () => { + // All apps are enabled by default. + let enabled = true; + return ({ + get enabled() { + return enabled; + }, + enable: jest.fn().mockImplementation(() => { + enabled = true; + }), + disable: jest.fn().mockImplementation(() => { + enabled = false; + }), + } as unknown) as jest.Mocked; + }; + const mockApps = new Map>([ + [usersManagementApp.id, getMockedApp()], + [rolesManagementApp.id, getMockedApp()], + [apiKeysManagementApp.id, getMockedApp()], + [roleMappingsManagementApp.id, getMockedApp()], + ] as Array<[string, jest.Mocked]>); + + service.start({ + management: { + sections: { + getSection: jest + .fn() + .mockReturnValue({ getApp: jest.fn().mockImplementation(id => mockApps.get(id)) }), + getAllSections: jest.fn(), + navigateToApp: jest.fn(), + }, + legacy: undefined, + }, + }); + + return { + mockApps, + updateFeatures(features: Partial) { + licenseSubject.next((features as unknown) as SecurityLicenseFeatures); + }, + }; + } + + it('does not do anything if `showLinks` is `true` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enable).not.toHaveBeenCalled(); + expect(mockApp.disable).not.toHaveBeenCalled(); + expect(mockApp.enabled).toBe(true); + } + }); + + it('disables all apps if `showLinks` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: false, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('disables only Role Mappings app if `showLinks` is `true`, but `showRoleMappingsManagement` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: false }); + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are disabled if `showLinks` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: false }); + + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are re-enabled if `showLinks` eventually transitions to `true` after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: true }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts new file mode 100644 index 00000000000000..5ad3681590fbf6 --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { + ManagementApp, + ManagementSetup, + ManagementStart, +} from '../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../common/licensing'; +import { AuthenticationServiceSetup } from '../authentication'; +import { PluginStartDependencies } from '../plugin'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; +import { rolesManagementApp } from './roles'; +import { usersManagementApp } from './users'; + +interface SetupParams { + management: ManagementSetup; + license: SecurityLicense; + authc: AuthenticationServiceSetup; + fatalErrors: FatalErrorsSetup; + getStartServices: CoreSetup['getStartServices']; +} + +interface StartParams { + management: ManagementStart; +} + +export class ManagementService { + private license!: SecurityLicense; + private licenseFeaturesSubscription?: Subscription; + + setup({ getStartServices, management, authc, license, fatalErrors }: SetupParams) { + this.license = license; + + const securitySection = management.sections.register({ + id: 'security', + title: i18n.translate('xpack.security.management.securityTitle', { + defaultMessage: 'Security', + }), + order: 100, + euiIconType: 'securityApp', + }); + + securitySection.registerApp(usersManagementApp.create({ authc, getStartServices })); + securitySection.registerApp( + rolesManagementApp.create({ fatalErrors, license, getStartServices }) + ); + securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); + securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); + } + + start({ management }: StartParams) { + this.licenseFeaturesSubscription = this.license.features$.subscribe(async features => { + const securitySection = management.sections.getSection('security')!; + + const securityManagementAppsStatuses: Array<[ManagementApp, boolean]> = [ + [securitySection.getApp(usersManagementApp.id)!, features.showLinks], + [securitySection.getApp(rolesManagementApp.id)!, features.showLinks], + [securitySection.getApp(apiKeysManagementApp.id)!, features.showLinks], + [ + securitySection.getApp(roleMappingsManagementApp.id)!, + features.showLinks && features.showRoleMappingsManagement, + ], + ]; + + // Iterate over all registered apps and update their enable status depending on the available + // license features. + for (const [app, enableStatus] of securityManagementAppsStatuses) { + if (app.enabled === enableStatus) { + continue; + } + + if (enableStatus) { + app.enable(); + } else { + app.disable(); + } + } + }); + } + + stop() { + if (this.licenseFeaturesSubscription) { + this.licenseFeaturesSubscription.unsubscribe(); + this.licenseFeaturesSubscription = undefined; + } + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts b/x-pack/plugins/security/public/management/management_urls.ts similarity index 74% rename from x-pack/legacy/plugins/security/public/views/management/management_urls.ts rename to x-pack/plugins/security/public/management/management_urls.ts index 881740c0b2895b..0d4e3fc920bdba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts +++ b/x-pack/plugins/security/public/management/management_urls.ts @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export const MANAGEMENT_PATH = '/management'; -export const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; +const MANAGEMENT_PATH = '/management'; +const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; export const ROLES_PATH = `${SECURITY_PATH}/roles`; export const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`; export const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`; export const USERS_PATH = `${SECURITY_PATH}/users`; export const EDIT_USERS_PATH = `${USERS_PATH}/edit`; -export const API_KEYS_PATH = `${SECURITY_PATH}/api_keys`; export const ROLE_MAPPINGS_PATH = `${SECURITY_PATH}/role_mappings`; -export const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; +const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; export const getEditRoleHref = (roleName: string) => - `#${EDIT_ROLES_PATH}/${encodeURIComponent(roleName)}`; + `#${ROLES_PATH}/edit/${encodeURIComponent(roleName)}`; export const getCreateRoleMappingHref = () => `#${CREATE_ROLE_MAPPING_PATH}`; diff --git a/x-pack/plugins/security/public/management/role_mappings/_index.scss b/x-pack/plugins/security/public/management/role_mappings/_index.scss new file mode 100644 index 00000000000000..bae6effcd2ec56 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/_index.scss @@ -0,0 +1 @@ +@import './edit_role_mapping/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx similarity index 64% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx index b826d68053e276..69142b1ad610e6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx @@ -5,24 +5,15 @@ */ import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { DeleteProvider } from '.'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { RoleMapping } from '../../../../../../common/model'; import { EuiConfirmModal } from '@elastic/eui'; -import { findTestSubject } from 'test_utils/find_test_subject'; import { act } from '@testing-library/react'; -import { toastNotifications } from 'ui/notify'; - -jest.mock('ui/notify', () => { - return { - toastNotifications: { - addError: jest.fn(), - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }; -}); +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { RoleMapping } from '../../../../../common/model'; +import { DeleteProvider } from '.'; + +import { roleMappingsAPIClientMock } from '../../index.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('DeleteProvider', () => { beforeEach(() => { @@ -30,17 +21,14 @@ describe('DeleteProvider', () => { }); it('allows a single role mapping to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([{ name: 'delete-me', success: true }]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -79,11 +67,10 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -94,21 +81,23 @@ describe('DeleteProvider', () => { }); it('allows multiple role mappings to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'delete-me-too', + success: true, + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'delete-me-too', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -152,11 +141,11 @@ describe('DeleteProvider', () => { 'delete-me', 'delete-me-too', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -167,22 +156,24 @@ describe('DeleteProvider', () => { }); it('handles mixed success/failure conditions', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'i-wont-work', + success: false, + error: new Error('something went wrong. sad.'), + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'i-wont-work', - success: false, - error: new Error('something went wrong. sad.'), - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -223,10 +214,9 @@ describe('DeleteProvider', () => { 'i-wont-work', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -235,8 +225,8 @@ describe('DeleteProvider', () => { ] `); - expect(notifications.addDanger).toHaveBeenCalledTimes(1); - expect(notifications.addDanger.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addDanger.mock.calls[0]).toMatchInlineSnapshot(` Array [ "Error deleting role mapping 'i-wont-work'", ] @@ -244,12 +234,13 @@ describe('DeleteProvider', () => { }); it('handles errors calling the API', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockRejectedValue(new Error('AHHHHH')); + + const notifications = coreMock.createStart().notifications; const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockImplementation(() => { - throw new Error('AHHHHH'); - }), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -284,12 +275,11 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(0); - expect(notifications.addError).toHaveBeenCalledTimes(1); - expect(notifications.addError.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addError.mock.calls[0]).toMatchInlineSnapshot(` Array [ [Error: AHHHHH], Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index 2072cedeab4628..860fe22cb8032c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -6,13 +6,14 @@ import React, { Fragment, useRef, useState, ReactElement } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../../common/model'; +import { RoleMappingsAPIClient } from '../../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; children: (deleteMappings: DeleteRoleMappings) => ReactElement; } @@ -23,7 +24,11 @@ export type DeleteRoleMappings = ( type OnSuccessCallback = (deletedRoleMappings: string[]) => void; -export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI, children }) => { +export const DeleteProvider: React.FunctionComponent = ({ + roleMappingsAPI, + children, + notifications, +}) => { const [roleMappings, setRoleMappings] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteInProgress, setIsDeleteInProgress] = useState(false); @@ -55,7 +60,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI try { result = await roleMappingsAPI.deleteRoleMappings(roleMappings.map(rm => rm.name)); } catch (e) { - toastNotifications.addError(e, { + notifications.toasts.addError(e, { title: i18n.translate( 'xpack.security.management.roleMappings.deleteRoleMapping.unknownError', { @@ -92,7 +97,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: successfulDeletes[0].name }, } ); - toastNotifications.addSuccess({ + notifications.toasts.addSuccess({ title: successMessage, 'data-test-subj': 'deletedRoleMappingSuccessToast', }); @@ -121,7 +126,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: erroredDeletes[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx index 969832b3ecbae1..5e14b0c179bfd7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NoCompatibleRealms: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NoCompatibleRealms: React.FunctionComponent = ({ docLinks }: Props) => ( ( defaultMessage="Role mappings will not be applied to any users. Contact your system administrator and refer to the {link} for more information." values={{ link: ( - + { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + + it('allows a role mapping to be created', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { + target: { value: 'my-role-mapping' }, + }); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'my-role-mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + all: [{ field: { username: '*' } }], + }, + metadata: {}, + }); + }); + + it('allows a role mapping to be updated', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + wrapper.find('button[id="addRuleOption"]').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'foo', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + any: [ + { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, + { field: { username: '*' } }, + ], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { source: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: false, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + }); + + it('renders the visual editor by default for simple rule sets', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: { + all: [ + { + field: { + username: '*', + }, + }, + { + field: { + dn: null, + }, + }, + { + field: { + realm: ['ldap', 'pki', null, 12], + }, + }, + ], + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + }); + + it('renders the JSON editor by default for complex rule sets', async () => { + const createRule = (depth: number): Record => { + if (depth > 0) { + const rule = { + all: [ + { + field: { + username: '*', + }, + }, + ], + } as Record; + + const subRule = createRule(depth - 1); + if (subRule) { + rule.all.push(subRule); + } + + return rule; + } + return null as any; + }; + + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: createRule(10), + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index b8a75a4ad9fdf7..142b53cbb50f29 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -19,20 +19,21 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; import { NoCompatibleRealms, PermissionDenied, DeleteProvider, SectionLoading, -} from '../../components'; -import { ROLE_MAPPINGS_PATH } from '../../../management_urls'; -import { validateRoleMappingForSave } from '../services/role_mapping_validation'; +} from '../components'; +import { RolesAPIClient } from '../../roles'; +import { ROLE_MAPPINGS_PATH } from '../../management_urls'; +import { validateRoleMappingForSave } from './services/role_mapping_validation'; import { MappingInfoPanel } from './mapping_info_panel'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface State { loadState: 'loading' | 'permissionDenied' | 'ready' | 'saveInProgress'; @@ -50,7 +51,10 @@ interface State { interface Props { name?: string; - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } export class EditRoleMappingPage extends Component { @@ -74,6 +78,12 @@ export class EditRoleMappingPage extends Component { this.loadAppData(); } + public async componentDidUpdate(prevProps: Props) { + if (prevProps.name !== this.props.name) { + await this.loadAppData(); + } + } + public render() { const { loadState } = this.state; @@ -101,6 +111,8 @@ export class EditRoleMappingPage extends Component { validateForm={this.state.validateForm} canUseInlineScripts={this.state.canUseInlineScripts} canUseStoredScripts={this.state.canUseStoredScripts} + rolesAPIClient={this.props.rolesAPIClient} + docLinks={this.props.docLinks} /> { }, }) } + docLinks={this.props.docLinks} /> {this.getFormButtons()} @@ -149,7 +162,7 @@ export class EditRoleMappingPage extends Component { values={{ learnMoreLink: ( @@ -166,7 +179,7 @@ export class EditRoleMappingPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -201,7 +214,10 @@ export class EditRoleMappingPage extends Component { {this.editingExistingRoleMapping() && ( - + {deleteRoleMappingsPrompt => { return ( { this.props.roleMappingsAPI .saveRoleMapping(this.state.roleMapping) .then(() => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.management.editRoleMapping.saveSuccess', { defaultMessage: `Saved role mapping '{roleMappingName}'`, values: { @@ -264,7 +280,7 @@ export class EditRoleMappingPage extends Component { this.backToRoleMappingsList(); }) .catch(e => { - toastNotifications.addError(e, { + this.props.notifications.toasts.addError(e, { title: i18n.translate('xpack.security.management.editRoleMapping.saveError', { defaultMessage: `Error saving role mapping`, }), @@ -312,7 +328,7 @@ export class EditRoleMappingPage extends Component { roleMapping, }); } catch (e) { - toastNotifications.addDanger({ + this.props.notifications.toasts.addDanger({ title: i18n.translate( 'xpack.security.management.editRoleMapping.table.fetchingRoleMappingsErrorMessage', { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx index d821b33ace6a7b..9b62ca27ca569c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx @@ -6,21 +6,27 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { MappingInfoPanel } from '.'; -import { RoleMapping } from '../../../../../../../common/model'; import { findTestSubject } from 'test_utils/find_test_subject'; +import { Role, RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; +import { DocumentationLinksService } from '../../documentation_links'; import { RoleSelector } from '../role_selector'; import { RoleTemplateEditor } from '../role_selector/role_template_editor'; +import { MappingInfoPanel } from '.'; -jest.mock('../../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('MappingInfoPanel', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('renders when creating a role mapping, default to the "roles" view', () => { const props = { roleMapping: { @@ -32,6 +38,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'create', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -77,6 +85,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -101,6 +111,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: true, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -140,6 +152,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: true, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -179,6 +193,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -202,6 +218,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index a02b4fc1709f02..02af6bfbafa7e0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -18,14 +18,15 @@ import { EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { validateRoleMappingName, validateRoleMappingRoles, validateRoleMappingRoleTemplates, -} from '../../services/role_mapping_validation'; +} from '../services/role_mapping_validation'; import { RoleSelector } from '../role_selector'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { roleMapping: RoleMapping; @@ -34,6 +35,8 @@ interface Props { validateForm: boolean; canUseInlineScripts: boolean; canUseStoredScripts: boolean; + rolesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; } interface State { @@ -163,6 +166,7 @@ export class MappingInfoPanel extends Component { > { defaultMessage="Create templates that describe the roles to assign to your users." />{' '} @@ -230,6 +234,7 @@ export class MappingInfoPanel extends Component { > { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { RolesAPIClient } from '../../../roles'; +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; describe('RoleSelector', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('allows roles to be selected, removing any previously selected role templates', () => { const props = { roleMapping: { @@ -36,6 +39,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'roles', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -57,6 +61,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -87,6 +92,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -122,6 +128,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx index 6b92d6b4907f16..992c2741ae93ee 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx @@ -7,12 +7,13 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; -import { RoleMapping, Role } from '../../../../../../../common/model'; -import { RolesApi } from '../../../../../../lib/roles_api'; +import { RoleMapping, Role } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { AddRoleTemplateButton } from './add_role_template_button'; import { RoleTemplateEditor } from './role_template_editor'; interface Props { + rolesAPIClient: PublicMethodsOf; roleMapping: RoleMapping; canUseInlineScripts: boolean; canUseStoredScripts: boolean; @@ -32,7 +33,7 @@ export class RoleSelector extends React.Component { } public async componentDidMount() { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx index 4b8d34d2719960..d79651d7b9cd6d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx @@ -18,12 +18,12 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RoleTemplate } from '../../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; import { isInlineRoleTemplate, isStoredRoleTemplate, isInvalidRoleTemplate, -} from '../../services/role_template_type'; +} from '../services/role_template_type'; import { RoleTemplateTypeSelect } from './role_template_type_select'; interface Props { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx similarity index 92% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx index 4a06af0fb436ba..aa65c5c9bcae74 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox } from '@elastic/eui'; -import { RoleTemplate } from '../../../../../../../common/model'; -import { isInlineRoleTemplate, isStoredRoleTemplate } from '../../services/role_template_type'; +import { RoleTemplate } from '../../../../../common/model'; +import { isInlineRoleTemplate, isStoredRoleTemplate } from '../services/role_template_type'; const templateTypeOptions = [ { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss new file mode 100644 index 00000000000000..c3b2764e647130 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss @@ -0,0 +1 @@ +@import './rule_editor_group'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx index 917b822acef3f2..d1411bd9bf2b97 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { AddRuleButton } from './add_rule_button'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { FieldRule, AllRule } from '../../../model'; +import { FieldRule, AllRule } from '../../model'; describe('AddRuleButton', () => { it('allows a field rule to be created', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx index 100c0dd3eeaee4..9696fa337a74fc 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Rule, FieldRule, AllRule } from '../../../model'; +import { Rule, FieldRule, AllRule } from '../../model'; interface Props { onClick: (newRule: Rule) => void; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx index 8d5d5c99ee99d7..5374f4625336de 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FieldRuleEditor } from './field_rule_editor'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { FieldRule } from '../../../model'; +import { FieldRule } from '../../model'; import { findTestSubject } from 'test_utils/find_test_subject'; import { ReactWrapper } from 'enzyme'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx index 52cf70dbd12bd2..2506c18dcaf3a0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx @@ -18,7 +18,7 @@ import { EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldRule, FieldRuleValue } from '../../../model'; +import { FieldRule, FieldRuleValue } from '../../model'; interface Props { rule: FieldRule; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index 8a9b37ab0f4065..263c456e901cd9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -17,7 +17,10 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { JSONRuleEditor } from './json_rule_editor'; import { EuiCodeEditor } from '@elastic/eui'; -import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('JSONRuleEditor', () => { it('renders an empty rule set', () => { @@ -25,6 +28,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -46,6 +50,7 @@ describe('JSONRuleEditor', () => { ]), onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -79,6 +84,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -100,6 +106,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -130,6 +137,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx index 371fb59f7a5d13..e7a9149513d208 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx @@ -11,13 +11,14 @@ import 'brace/theme/github'; import { EuiCodeEditor, EuiFormRow, EuiButton, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../../model'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; +import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../model'; interface Props { rules: Rule | null; onChange: (updatedRules: Rule | null) => void; onValidityChange: (isValid: boolean) => void; + docLinks: DocumentationLinksService; } export const JSONRuleEditor = (props: Props) => { @@ -107,7 +108,7 @@ export const JSONRuleEditor = (props: Props) => { values={{ roleMappingAPI: ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx similarity index 88% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index 809264183d30ca..b9c650cc1f77a4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -15,8 +15,11 @@ import { findTestSubject } from 'test_utils/find_test_subject'; // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. import 'test_utils/stub_web_worker'; -import { AllRule, FieldRule } from '../../../model'; +import { AllRule, FieldRule } from '../../model'; import { EuiErrorBoundary } from '@elastic/eui'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('RuleEditorPanel', () => { it('renders the visual editor when no rules are defined', () => { @@ -25,6 +28,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -45,6 +49,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -68,6 +73,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -103,6 +109,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx index 4aab49b2b2efcf..6e6641caa1f399 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx @@ -22,19 +22,20 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { VisualRuleEditor } from './visual_rule_editor'; import { JSONRuleEditor } from './json_rule_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, generateRulesFromRaw } from '../../../model'; -import { validateRoleMappingRules } from '../../services/role_mapping_validation'; -import { documentationLinks } from '../../../services/documentation_links'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, generateRulesFromRaw } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; +import { validateRoleMappingRules } from '../services/role_mapping_validation'; interface Props { rawRules: RoleMapping['rules']; onChange: (rawRules: RoleMapping['rules']) => void; onValidityChange: (isValid: boolean) => void; validateForm: boolean; + docLinks: DocumentationLinksService; } interface State { @@ -91,7 +92,7 @@ export class RuleEditorPanel extends Component { values={{ learnMoreLink: ( @@ -214,6 +215,7 @@ export class RuleEditorPanel extends Component { rules={this.state.rules} onChange={this.onRuleChange} onValidityChange={this.onValidityChange} + docLinks={this.props.docLinks} /> ); default: diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx index 3e0e0e386e98c2..5946aac4306b1f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { RuleGroupEditor } from './rule_group_editor'; import { shallowWithIntl, mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../../model'; +import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../model'; import { FieldRuleEditor } from './field_rule_editor'; import { AddRuleButton } from './add_rule_button'; import { EuiContextMenuItem } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx index 6fb33db179e8a6..c17a853a654676 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx @@ -16,8 +16,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AddRuleButton } from './add_rule_button'; import { RuleGroupTitle } from './rule_group_title'; import { FieldRuleEditor } from './field_rule_editor'; -import { RuleGroup, Rule, FieldRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { RuleGroup, Rule, FieldRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx index e46893afd4d86e..6bef9c09eeef39 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx @@ -15,14 +15,7 @@ import { EuiConfirmModal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - RuleGroup, - AllRule, - AnyRule, - ExceptAllRule, - ExceptAnyRule, - FieldRule, -} from '../../../model'; +import { RuleGroup, AllRule, AnyRule, ExceptAllRule, ExceptAnyRule, FieldRule } from '../../model'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx index 7c63613ee1cc9e..b40f0063acb08a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { VisualRuleEditor } from './visual_rule_editor'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; import { RuleGroupEditor } from './rule_group_editor'; import { FieldRuleEditor } from './field_rule_editor'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx index 214c583189fb80..2e3db275788ee4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx @@ -9,9 +9,9 @@ import { EuiEmptyPrompt, EuiCallOut, EuiSpacer, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldRuleEditor } from './field_rule_editor'; import { RuleGroupEditor } from './rule_group_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, FieldRule, RuleGroup, AllRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, FieldRule, RuleGroup, AllRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rules: Rule | null; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts index 9614c4338b631f..0c3f988ae6b105 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts @@ -10,7 +10,7 @@ import { validateRoleMappingRules, validateRoleMappingForSave, } from './role_mapping_validation'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; describe('validateRoleMappingName', () => { it('requires a value', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts index 5916d6fd9e1891..7695f1da14881a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { generateRulesFromRaw } from '../../model'; interface ValidationResult { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts index 8e1f47a4157ae9..c093bb1b3fbbc9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts @@ -9,7 +9,7 @@ import { isInlineRoleTemplate, isInvalidRoleTemplate, } from './role_template_type'; -import { RoleTemplate } from '../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; describe('#isStoredRoleTemplate', () => { it('returns true for stored templates, false otherwise', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts index 90d8d1a09e5877..5e646535a6c4d5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts @@ -9,7 +9,7 @@ import { StoredRoleTemplate, InlineRoleTemplate, InvalidRoleTemplate, -} from '../../../../../../common/model'; +} from '../../../../../common/model'; export function isStoredRoleTemplate( roleMappingTemplate: RoleTemplate diff --git a/x-pack/plugins/security/public/management/role_mappings/index.mock.ts b/x-pack/plugins/security/public/management/role_mappings/index.mock.ts new file mode 100644 index 00000000000000..826477a1a5b153 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/index.mock.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { roleMappingsAPIClientMock } from './role_mappings_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/role_mappings/index.ts b/x-pack/plugins/security/public/management/role_mappings/index.ts new file mode 100644 index 00000000000000..f670ea61810386 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { roleMappingsManagementApp } from './role_mappings_management_app'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap b/x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap rename to x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts b/x-pack/plugins/security/public/management/role_mappings/model/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts rename to x-pack/plugins/security/public/management/role_mappings/model/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts index ebd48f6d15d99e..ad486a8b314c4f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts @@ -5,7 +5,7 @@ */ import { generateRulesFromRaw, FieldRule } from '.'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { RuleBuilderError } from './rule_builder_error'; describe('generateRulesFromRaw', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts index fe344b2ae38dd0..a384e61e521aba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { FieldRule, FieldRuleValue } from './field_rule'; import { AllRule } from './all_rule'; import { AnyRule } from './any_rule'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts index 3e1e7fad9b36f5..5077c79a543c46 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts @@ -7,7 +7,7 @@ import { Rule } from './rule'; /** - * Represents a catagory of Role Mapping rules which are capable of containing other rules. + * Represents a category of Role Mapping rules which are capable of containing other rules. */ export abstract class RuleGroup extends Rule { /** diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts new file mode 100644 index 00000000000000..07d583d1e983f6 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const roleMappingsAPIClientMock = { + create: () => ({ + checkRoleMappingFeatures: jest.fn(), + getRoleMappings: jest.fn(), + getRoleMapping: jest.fn(), + saveRoleMapping: jest.fn(), + deleteRoleMappings: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts similarity index 89% rename from x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts index b8bcba91388b53..0a88ed1da9ac3a 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; -import { RoleMapping } from '../../common/model'; +import { HttpStart } from 'src/core/public'; +import { RoleMapping } from '../../../common/model'; interface CheckRoleMappingFeaturesResponse { canManageRoleMappings: boolean; @@ -20,8 +20,8 @@ type DeleteRoleMappingsResponse = Array<{ error?: Error; }>; -export class RoleMappingsAPI { - constructor(private readonly http: CoreSetup['http']) {} +export class RoleMappingsAPIClient { + constructor(private readonly http: HttpStart) {} public async checkRoleMappingFeatures(): Promise { return this.http.get(`/internal/security/_check_role_mapping_features`); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx index 2342eeb97d03e3..6fe4bcc7a0bbb0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getCreateRoleMappingHref } from '../../../../management_urls'; +import { getCreateRoleMappingHref } from '../../../management_urls'; export const CreateRoleMappingButton = () => { return ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx new file mode 100644 index 00000000000000..de0722b4cd85e2 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { RoleMappingsGridPage } from '.'; +import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../components'; +import { EmptyPrompt } from './empty_prompt'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiLink } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { DocumentationLinksService } from '../documentation_links'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; + +describe('RoleMappingsGridPage', () => { + it('renders an empty prompt when no role mappings exist', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(EmptyPrompt)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(EmptyPrompt)).toHaveLength(1); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: [], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders links to mapped roles', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); + expect(links).toHaveLength(1); + expect(links.at(0).props()).toMatchObject({ + href: '#/management/security/roles/edit/superuser', + }); + }); + + it('describes the number of mapped role templates', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + role_templates: [{}, {}], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const templates = findTestSubject(wrapper, 'roleMappingRoles'); + expect(templates).toHaveLength(1); + expect(templates.text()).toEqual(`2 role templates defined`); + }); + + it('allows role mappings to be deleted, refreshing the grid after', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); + expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); + + findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); + expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); + // Expect an additional API call to refresh the grid + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index 7b23f2288d1ba6..feb918cb6b301f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -25,24 +25,27 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { NoCompatibleRealms, DeleteProvider, PermissionDenied, SectionLoading, -} from '../../components'; -import { documentationLinks } from '../../services/documentation_links'; +} from '../components'; import { getCreateRoleMappingHref, getEditRoleMappingHref, getEditRoleHref, -} from '../../../management_urls'; +} from '../../management_urls'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } interface State { @@ -140,7 +143,7 @@ export class RoleMappingsGridPage extends Component { values={{ learnMoreLink: ( @@ -168,7 +171,7 @@ export class RoleMappingsGridPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -214,7 +217,10 @@ export class RoleMappingsGridPage extends Component { const search = { toolsLeft: selectedItems.length ? ( - + {deleteRoleMappingsPrompt => { return ( { return ( - + {deleteRoleMappingPrompt => { return ( ({ + RoleMappingsGridPage: (props: any) => `Role Mappings Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role_mapping', () => ({ + EditRoleMappingPage: (props: any) => `Role Mapping Edit Page: ${JSON.stringify(props)}`, +})); + +import { roleMappingsManagementApp } from './role_mappings_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await roleMappingsManagementApp + .create({ getStartServices: coreMock.createSetup().getStartServices as any }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('roleMappingsManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + roleMappingsManagementApp.create({ + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "role_mappings", + "mount": [Function], + "order": 40, + "title": "Role Mappings", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mappings Page: {"notifications":{"toasts":{}},"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'someRoleMappingName'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { href: `#/some-base-path/role_mappings/edit/${roleMappingName}`, text: roleMappingName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"name":"someRoleMappingName","roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role mapping name in `edit role mapping` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'some 安全性 role mapping'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { + href: + '#/some-base-path/role_mappings/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', + text: roleMappingName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx new file mode 100644 index 00000000000000..af1572cedbadef --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { RoleMappingsAPIClient } from './role_mappings_api_client'; +import { DocumentationLinksService } from './documentation_links'; +import { RoleMappingsGridPage } from './role_mappings_grid'; +import { EditRoleMappingPage } from './edit_role_mapping'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const roleMappingsManagementApp = Object.freeze({ + id: 'role_mappings', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 40, + title: i18n.translate('xpack.security.management.roleMappingsTitle', { + defaultMessage: 'Role Mappings', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + const roleMappingsBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roleMapping.breadcrumb', { + defaultMessage: 'Role Mappings', + }), + href: `#${basePath}`, + }, + ]; + + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); + const dockLinksService = new DocumentationLinksService(docLinks); + const RoleMappingsGridPageWithBreadcrumbs = () => { + setBreadcrumbs(roleMappingsBreadcrumbs); + return ( + + ); + }; + + const EditRoleMappingsPageWithBreadcrumbs = () => { + const { name } = useParams<{ name?: string }>(); + + setBreadcrumbs([ + ...roleMappingsBreadcrumbs, + name + ? { text: name, href: `#${basePath}/edit/${encodeURIComponent(name)}` } + : { + text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss new file mode 100644 index 00000000000000..5256c79f01f10f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/_index.scss @@ -0,0 +1 @@ +@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/documentation_links.ts b/x-pack/plugins/security/public/management/roles/documentation_links.ts new file mode 100644 index 00000000000000..cf46973d3541ca --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/documentation_links.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DocLinksStart } from 'src/core/public'; + +export class DocumentationLinksService { + private readonly esDocBasePath: string; + + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getESClusterPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-cluster`; + } + + public getESRunAsPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#_run_as_privilege`; + } + + public getESIndicesPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-indices`; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap b/x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap rename to x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss new file mode 100644 index 00000000000000..0153b1734ceba0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss @@ -0,0 +1,3 @@ +@import './collapsible_panel/index'; +@import './spaces_popover_list/index'; +@import './privileges/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss new file mode 100644 index 00000000000000..c0f4f8ab9a8701 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss @@ -0,0 +1 @@ +@import './collapsible_panel'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 416dd7f6c4e5c7..01af7cb4509f64 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -50,7 +50,6 @@ export class CollapsiblePanel extends Component { public getTitle = () => { return ( - // @ts-ignore diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx index cc16866c883555..f4af935be66487 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteRoleButton } from './delete_role_button'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx index 1ae84d3fb72242..c6a10396f235c4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, - // @ts-ignore - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx new file mode 100644 index 00000000000000..2b3d7c811f6de3 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -0,0 +1,552 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ReactWrapper } from 'enzyme'; +import React from 'react'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +// These modules should be moved into a common directory +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Actions } from '../../../../server/authorization/actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { privilegesFactory } from '../../../../server/authorization/privileges'; +import { Role } from '../../../../common/model'; +import { DocumentationLinksService } from '../documentation_links'; +import { EditRolePage } from './edit_role_page'; +import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; +import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; + +import { TransformErrorSection } from './privileges/kibana/transform_error_section'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { licenseMock } from '../../../../common/licensing/index.mock'; +import { userAPIClientMock } from '../../users/index.mock'; +import { rolesAPIClientMock, indicesAPIClientMock, privilegesAPIClientMock } from '../index.mock'; + +const buildFeatures = () => { + return [ + { + id: 'feature1', + name: 'Feature 1', + icon: 'addDataApp', + app: ['feature1App'], + privileges: { + all: { + app: ['feature1App'], + ui: ['feature1-ui'], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, + { + id: 'feature2', + name: 'Feature 2', + icon: 'addDataApp', + app: ['feature2App'], + privileges: { + all: { + app: ['feature2App'], + ui: ['feature2-ui'], + savedObject: { + all: ['feature2'], + read: ['config'], + }, + }, + }, + }, + ] as Feature[]; +}; + +const buildRawKibanaPrivileges = () => { + return privilegesFactory(new Actions('unit_test_version'), { + getFeatures: () => buildFeatures(), + }).get(); +}; + +const buildBuiltinESPrivileges = () => { + return { + cluster: ['all', 'manage', 'monitor'], + index: ['all', 'read', 'write', 'index'], + }; +}; + +const buildUICapabilities = (canManageSpaces = true) => { + return { + catalogue: {}, + management: {}, + navLinks: {}, + spaces: { + manage: canManageSpaces, + }, + } as Capabilities; +}; + +const buildSpaces = () => { + return [ + { + id: 'default', + name: 'Default', + disabledFeatures: [], + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + disabledFeatures: [], + }, + { + id: 'space_2', + name: 'Space 2', + disabledFeatures: ['feature2'], + }, + ] as Space[]; +}; + +const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); +}; + +const expectSaveFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); +}; + +function getProps({ + action, + role, + canManageSpaces = true, + spacesEnabled = true, +}: { + action: 'edit' | 'clone'; + role?: Role; + canManageSpaces?: boolean; + spacesEnabled?: boolean; +}) { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRole.mockResolvedValue(role); + + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + + const indicesAPIClient = indicesAPIClientMock.create(); + + const userAPIClient = userAPIClientMock.create(); + userAPIClient.getUsers.mockResolvedValue([]); + + const privilegesAPIClient = privilegesAPIClientMock.create(); + privilegesAPIClient.getAll.mockResolvedValue(buildRawKibanaPrivileges()); + privilegesAPIClient.getBuiltIn.mockResolvedValue(buildBuiltinESPrivileges()); + + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleDocumentLevelSecurity: true, + allowRoleFieldLevelSecurity: true, + } as any); + + const { fatalErrors } = coreMock.createSetup(); + const { http, docLinks, notifications } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + return buildFeatures(); + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + return { + action, + roleName: role?.name, + license, + http, + indexPatterns, + indicesAPIClient, + privilegesAPIClient, + rolesAPIClient, + userAPIClient, + notifications, + docLinks: new DocumentationLinksService(docLinks), + fatalErrors, + spacesEnabled, + uiCapabilities: buildUICapabilities(canManageSpaces), + }; +} + +describe('', () => { + describe('with spaces enabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('renders an auth error when not authorized to manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(1); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + describe('with spaces disabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('does not care if user cannot manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(0); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + it('can render if features are not available', async () => { + const { http } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + const error = { response: { status: 404 } }; + throw error; + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render if index patterns are not available', async () => { + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockRejectedValue({ response: { status: 403 } }); + + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx new file mode 100644 index 00000000000000..33e69a68ca8960 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -0,0 +1,594 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _, { get } from 'lodash'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { + ChangeEvent, + Fragment, + FunctionComponent, + HTMLProps, + useEffect, + useRef, + useState, +} from 'react'; +import { + Capabilities, + FatalErrorsSetup, + HttpStart, + IHttpFetchError, + NotificationsStart, +} from 'src/core/public'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +import { + KibanaPrivileges, + RawKibanaPrivileges, + Role, + BuiltinESPrivileges, + isReadOnlyRole as checkIfRoleReadOnly, + isReservedRole as checkIfRoleReserved, + copyRole, + prepareRoleClone, + RoleIndexPrivilege, +} from '../../../../common/model'; +import { ROLES_PATH } from '../../management_urls'; +import { RoleValidationResult, RoleValidator } from './validate_role'; +import { DeleteRoleButton } from './delete_role_button'; +import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; +import { ReservedRoleBadge } from './reserved_role_badge'; +import { SecurityLicense } from '../../../../common/licensing'; +import { UserAPIClient } from '../../users'; +import { DocumentationLinksService } from '../documentation_links'; +import { IndicesAPIClient } from '../indices_api_client'; +import { RolesAPIClient } from '../roles_api_client'; +import { PrivilegesAPIClient } from '../privileges_api_client'; + +interface Props { + action: 'edit' | 'clone'; + roleName?: string; + indexPatterns: IndexPatternsContract; + userAPIClient: PublicMethodsOf; + indicesAPIClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + privilegesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + http: HttpStart; + license: SecurityLicense; + spacesEnabled: boolean; + uiCapabilities: Capabilities; + notifications: NotificationsStart; + fatalErrors: FatalErrorsSetup; +} + +function useRunAsUsers( + userAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [userNames, setUserNames] = useState(null); + useEffect(() => { + userAPIClient.getUsers().then( + users => setUserNames(users.map(user => user.username)), + err => fatalErrors.add(err) + ); + }, [fatalErrors, userAPIClient]); + + return userNames; +} + +function useIndexPatternsTitles( + indexPatterns: IndexPatternsContract, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart +) { + const [indexPatternsTitles, setIndexPatternsTitles] = useState(null); + useEffect(() => { + indexPatterns + .getTitles() + .catch((err: IHttpFetchError) => { + // If user doesn't have access to the index patterns they still should be able to create new + // or edit existing role. + if (err.response?.status === 403) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.noIndexPatternsPermission', { + defaultMessage: 'You need permission to access the list of available index patterns.', + }), + }); + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setIndexPatternsTitles); + }, [fatalErrors, indexPatterns, notifications]); + + return indexPatternsTitles; +} + +function usePrivileges( + privilegesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [privileges, setPrivileges] = useState<[RawKibanaPrivileges, BuiltinESPrivileges] | null>( + null + ); + useEffect(() => { + Promise.all([ + privilegesAPIClient.getAll({ includeActions: true }), + privilegesAPIClient.getBuiltIn(), + ]).then( + ([kibanaPrivileges, builtInESPrivileges]) => + setPrivileges([kibanaPrivileges, builtInESPrivileges]), + err => fatalErrors.add(err) + ); + }, [privilegesAPIClient, fatalErrors]); + + return privileges; +} + +function useRole( + rolesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart, + license: SecurityLicense, + action: string, + roleName?: string +) { + const [role, setRole] = useState(null); + useEffect(() => { + const rolePromise = roleName + ? rolesAPIClient.getRole(roleName) + : Promise.resolve({ + name: '', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + _unrecognized_applications: [], + } as Role); + + rolePromise + .then(fetchedRole => { + if (action === 'clone' && checkIfRoleReserved(fetchedRole)) { + backToRoleList(); + return; + } + + if (fetchedRole.elasticsearch.indices.length === 0) { + const emptyOption: RoleIndexPrivilege = { + names: [], + privileges: [], + }; + + const { + allowRoleDocumentLevelSecurity, + allowRoleFieldLevelSecurity, + } = license.getFeatures(); + + if (allowRoleFieldLevelSecurity) { + emptyOption.field_security = { + grant: ['*'], + except: [], + }; + } + + if (allowRoleDocumentLevelSecurity) { + emptyOption.query = ''; + } + + fetchedRole.elasticsearch.indices.push(emptyOption); + } + + setRole(action === 'clone' ? prepareRoleClone(fetchedRole) : copyRole(fetchedRole)); + }) + .catch((err: IHttpFetchError) => { + if (err.response?.status === 404) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.roleNotFound', { + defaultMessage: 'No "{roleName}" role found.', + values: { roleName }, + }), + }); + backToRoleList(); + } else { + fatalErrors.add(err); + } + }); + }, [roleName, action, fatalErrors, rolesAPIClient, notifications, license]); + + return [role, setRole] as [Role | null, typeof setRole]; +} + +function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) { + const [spaces, setSpaces] = useState(null); + useEffect(() => { + (spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then( + fetchedSpaces => setSpaces(fetchedSpaces), + err => fatalErrors.add(err) + ); + }, [http, fatalErrors, spacesEnabled]); + + return spaces; +} + +function useFeatures(http: HttpStart, fatalErrors: FatalErrorsSetup) { + const [features, setFeatures] = useState(null); + useEffect(() => { + http + .get('/api/features') + .catch((err: IHttpFetchError) => { + // Currently, the `/api/features` endpoint effectively requires the "Global All" kibana privilege (e.g., what + // the `kibana_user` grants), because it returns information about all registered features (#35841). It's + // possible that a user with `manage_security` will attempt to visit the role management page without the + // correct Kibana privileges. If that's the case, then they receive a partial view of the role, and the UI does + // not allow them to make changes to that role's kibana privileges. When this user visits the edit role page, + // this API endpoint will throw a 404, which causes view to fail completely. So we instead attempt to detect the + // 404 here, and respond in a way that still allows the UI to render itself. + const unauthorizedForFeatures = err.response?.status === 404; + if (unauthorizedForFeatures) { + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setFeatures); + }, [http, fatalErrors]); + + return features; +} + +function backToRoleList() { + window.location.hash = ROLES_PATH; +} + +export const EditRolePage: FunctionComponent = ({ + userAPIClient, + indexPatterns, + rolesAPIClient, + indicesAPIClient, + privilegesAPIClient, + http, + roleName, + action, + fatalErrors, + spacesEnabled, + license, + docLinks, + uiCapabilities, + notifications, +}) => { + // We should keep the same mutable instance of Validator for every re-render since we'll + // eventually enable validation after the first time user tries to save a role. + const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); + + const [formError, setFormError] = useState(null); + const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); + const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); + const privileges = usePrivileges(privilegesAPIClient, fatalErrors); + const spaces = useSpaces(http, fatalErrors, spacesEnabled); + const features = useFeatures(http, fatalErrors); + const [role, setRole] = useRole( + rolesAPIClient, + fatalErrors, + notifications, + license, + action, + roleName + ); + + if (!role || !runAsUsers || !indexPatternsTitles || !privileges || !spaces || !features) { + return null; + } + + const isEditingExistingRole = !!roleName && action === 'edit'; + const isReadOnlyRole = checkIfRoleReadOnly(role); + const isReservedRole = checkIfRoleReserved(role); + + const [kibanaPrivileges, builtInESPrivileges] = privileges; + + const getFormTitle = () => { + let titleText; + const props: HTMLProps = { + tabIndex: 0, + }; + if (isReservedRole) { + titleText = ( + + ); + props['aria-describedby'] = 'reservedRoleDescription'; + } else if (isEditingExistingRole) { + titleText = ( + + ); + } else { + titleText = ( + + ); + } + + return ( + +

+ {titleText} +

+
+ ); + }; + + const getActionButton = () => { + if (isEditingExistingRole && !isReadOnlyRole) { + return ( + + + + ); + } + + return null; + }; + + const getRoleName = () => { + return ( + + + } + helpText={ + !isReservedRole && isEditingExistingRole ? ( + + ) : ( + undefined + ) + } + {...validator.validateRoleName(role)} + > + + + + ); + }; + + const onNameChange = (e: ChangeEvent) => + setRole({ + ...role, + name: e.target.value.replace(/\s/g, '_'), + }); + + const getElasticsearchPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const onRoleChange = (newRole: Role) => setRole(newRole); + + const getKibanaPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const getFormButtons = () => { + if (isReadOnlyRole) { + return getReturnToRoleListButton(); + } + + return ( + + {getSaveButton()} + {getCancelButton()} + + {getActionButton()} + + ); + }; + + const getReturnToRoleListButton = () => { + return ( + + + + ); + }; + + const getSaveButton = () => { + const saveText = isEditingExistingRole ? ( + + ) : ( + + ); + + return ( + + {saveText} + + ); + }; + + const getCancelButton = () => { + return ( + + + + ); + }; + + const saveRole = async () => { + validator.enableValidation(); + + const result = validator.validateForSave(role); + if (result.isInvalid) { + setFormError(result); + } else { + setFormError(null); + + try { + await rolesAPIClient.saveRole({ role, spacesEnabled }); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', + { defaultMessage: 'Saved role' } + ) + ); + + backToRoleList(); + } + }; + + const handleDeleteRole = async () => { + try { + await rolesAPIClient.deleteRole(role.name); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role' } + ) + ); + + backToRoleList(); + }; + + const description = spacesEnabled ? ( + + ) : ( + + ); + + return ( +
+ + {getFormTitle()} + + + + {description} + + {isReservedRole && ( + + + +

+ +

+
+
+ )} + + + + {getRoleName()} + + {getElasticsearchPrivileges()} + + {getKibanaPrivileges()} + + + + {getFormButtons()} +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts similarity index 93% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts index 74bde71dc421ab..3fd8536951967a 100644 --- a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RoleKibanaPrivilege } from '../../common/model'; +import { RoleKibanaPrivilege } from '../../../../common/model'; /** * Determines if the passed privilege spec defines global privileges. diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss new file mode 100644 index 00000000000000..a1a9d038065e61 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss @@ -0,0 +1,2 @@ +@import './privilege_feature_icon'; +@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss new file mode 100644 index 00000000000000..a7f24c96a28216 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss @@ -0,0 +1,4 @@ +.secPrivilegeFeatureIcon { + flex-shrink: 0; + margin-right: $euiSizeS; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 795131337c31fd..323629de7578d7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -15,7 +15,7 @@ exports[`it renders without crashing 1`] = ` /> { + expect(shallowWithIntl()).toMatchSnapshot(); +}); + +test('it renders ClusterPrivileges', () => { + expect( + mountWithIntl().find(ClusterPrivileges) + ).toHaveLength(1); +}); + +test('it renders IndexPrivileges', () => { + expect( + mountWithIntl().find(IndexPrivileges) + ).toHaveLength(1); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index c0e6db3fef21c8..96249ccf3ff87e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -7,7 +7,6 @@ import { EuiButton, EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiHorizontalRule, @@ -19,26 +18,26 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Role, BuiltinESPrivileges } from '../../../../../../../common/model'; -// @ts-ignore -import { documentationLinks } from '../../../../../../documentation_links'; -import { RoleValidator } from '../../../lib/validate_role'; +import { Role, BuiltinESPrivileges } from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { CollapsiblePanel } from '../../collapsible_panel'; import { ClusterPrivileges } from './cluster_privileges'; - import { IndexPrivileges } from './index_privileges'; +import { DocumentationLinksService } from '../../../documentation_links'; interface Props { role: Role; editable: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + license: SecurityLicense; onChange: (role: Role) => void; runAsUsers: string[]; validator: RoleValidator; builtinESPrivileges: BuiltinESPrivileges; indexPatterns: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; } export class ElasticsearchPrivileges extends Component { @@ -53,23 +52,22 @@ export class ElasticsearchPrivileges extends Component { public getForm = () => { const { role, - httpClient, + indicesAPIClient, + docLinks, validator, onChange, editable, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, builtinESPrivileges, } = this.props; const indexProps = { role, - httpClient, + indicesAPIClient, validator, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, onChange, availableIndexPrivileges: builtinESPrivileges.index, }; @@ -91,7 +89,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription" defaultMessage="Manage the actions this role can perform against your cluster. " /> - {this.learnMore(documentationLinks.esClusterPrivileges)} + {this.learnMore(docLinks.getESClusterPrivilegesDocUrl())}

} > @@ -121,7 +119,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription" defaultMessage="Allow requests to be submitted on the behalf of other users. " /> - {this.learnMore(documentationLinks.esRunAsPrivileges)} + {this.learnMore(docLinks.getESRunAsPrivilegesDocUrl())}

} > @@ -165,7 +163,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription" defaultMessage="Control access to the data in your cluster. " /> - {this.learnMore(documentationLinks.esIndicesPrivileges)} + {this.learnMore(docLinks.getESIndicesPrivilegesDocUrl())}

diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 6d386fd78a11b1..5e2da513143653 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiTextArea } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; test('it renders without crashing', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index bafc56dc167ea5..15e0367c2b6dc1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { RoleIndexPrivilege } from '../../../../../../../common/model'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleIndexPrivilege } from '../../../../../../common/model'; +import { RoleValidator } from '../../validate_role'; const fromOption = (option: any) => option.label; const toOption = (value: string) => ({ label: value }); @@ -164,7 +164,6 @@ export class IndexPrivilegeForm extends Component { {!isReadOnlyRole && ( { - // @ts-ignore missing "compressed" prop definition { } return ( - // @ts-ignore {!this.props.isReadOnlyRole && ( { - // @ts-ignore missing "compressed" proptype new Promise(setImmediate); test('it renders without crashing', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -25,14 +34,13 @@ test('it renders without crashing', async () => { run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = shallowWithIntl(); await flushPromises(); @@ -40,6 +48,12 @@ test('it renders without crashing', async () => { }); test('it renders a IndexPrivilegeForm for each privilege on the role', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -59,14 +73,13 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = mountWithIntl(); await flushPromises(); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx similarity index 84% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index f09084ad2bb382..2c745067fede2d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,19 +5,23 @@ */ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { Role, RoleIndexPrivilege } from '../../../../../../../common/model'; -import { isReadOnlyRole, isRoleEnabled } from '../../../../../../lib/role_utils'; -import { getFields } from '../../../../../../objects'; -import { RoleValidator } from '../../../lib/validate_role'; +import { + Role, + RoleIndexPrivilege, + isReadOnlyRole, + isRoleEnabled, +} from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; interface Props { role: Role; indexPatterns: string[]; availableIndexPrivileges: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + license: SecurityLicense; onChange: (role: Role) => void; validator: RoleValidator; } @@ -43,20 +47,16 @@ export class IndexPrivileges extends Component { public render() { const { indices = [] } = this.props.role.elasticsearch; - const { - indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, - availableIndexPrivileges, - } = this.props; + const { indexPatterns, license, availableIndexPrivileges } = this.props; + const { allowRoleDocumentLevelSecurity, allowRoleFieldLevelSecurity } = license.getFeatures(); const props = { indexPatterns, // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that // doesn't permit FLS/DLS). - allowDocumentLevelSecurity: allowDocumentLevelSecurity || !isRoleEnabled(this.props.role), - allowFieldLevelSecurity: allowFieldLevelSecurity || !isRoleEnabled(this.props.role), + allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role), + allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role), isReadOnlyRole: isReadOnlyRole(this.props.role), }; @@ -171,7 +171,7 @@ export class IndexPrivileges extends Component { try { return { - [pattern]: await getFields(this.props.httpClient, pattern), + [pattern]: await this.props.indicesAPIClient.getFields(pattern), }; } catch (e) { return { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss new file mode 100644 index 00000000000000..19547c0e1953e0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss @@ -0,0 +1,2 @@ +@import './feature_table/index'; +@import './space_aware_privilege_section/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss new file mode 100644 index 00000000000000..6a96553742819f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss @@ -0,0 +1 @@ +@import './change_all_privileges'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 9648bf1d111bf4..dea42e16f99d42 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { FeatureTable } from './feature_table'; const defaultPrivilegeDefinition = new KibanaPrivileges({ @@ -113,7 +112,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={0} - intl={null as any} /> ); @@ -141,7 +139,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={-1} - intl={null as any} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index a05dc687fce4ac..8283efe23260af 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { - // @ts-ignore EuiButtonGroup, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiText, IconType, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; import { AllowedPrivilege, CalculatedPrivilege, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { NO_PRIVILEGE_VALUE } from '../constants'; import { PrivilegeDisplay } from '../space_aware_privilege_section/privilege_display'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; @@ -36,7 +35,6 @@ interface Props { allowedPrivileges: AllowedPrivilege; rankedFeaturePrivileges: FeaturesPrivileges; kibanaPrivileges: KibanaPrivileges; - intl: InjectedIntl; spacesIndex: number; onChange: (featureId: string, privileges: string[]) => void; onChangeAll: (privileges: string[]) => void; @@ -100,9 +98,7 @@ export class FeatureTable extends Component { const availablePrivileges = Object.values(rankedFeaturePrivileges)[0]; return ( - // @ts-ignore missing responsive from typedef { private getColumns = (availablePrivileges: string[]) => [ { field: 'feature', - name: this.props.intl.formatMessage({ - id: 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', - defaultMessage: 'Feature', - }), + name: i18n.translate( + 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', + { defaultMessage: 'Feature' } + ), render: (feature: TableFeature) => { let tooltipElement = null; if (feature.privilegesTooltip) { @@ -239,9 +235,7 @@ export class FeatureTable extends Component { }); return ( - // @ts-ignore missing name from typedef void; validator: RoleValidator; - intl: InjectedIntl; } export class KibanaPrivilegesRegion extends Component { @@ -81,7 +79,6 @@ export class KibanaPrivilegesRegion extends Component { privilegeCalculatorFactory={privilegeCalculatorFactory} onChange={onChange} editable={editable} - intl={this.props.intl} /> ); } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx index 135419cc9a10d2..bda0227372c094 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx @@ -6,7 +6,7 @@ import { EuiSelect } from '@elastic/eui'; import React, { ChangeEvent, Component } from 'react'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props { ['data-test-subj']: string; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index f97fa93294ff5b..db1e3cfd616212 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -3,13 +3,13 @@ * 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 + import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { SimplePrivilegeSection } from './simple_privilege_section'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx index 7768dc769a32f7..2221fc6bab2797 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -6,21 +6,24 @@ import { EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiSuperSelect, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { Feature } from '../../../../../../../../features/public'; +import { + KibanaPrivileges, + Role, + RoleKibanaPrivilege, + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; @@ -31,7 +34,6 @@ interface Props { features: Feature[]; onChange: (role: Role) => void; editable: boolean; - intl: InjectedIntl; } interface State { @@ -230,7 +232,6 @@ export class SimplePrivilegeSection extends Component { allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} features={this.props.features} - intl={this.props.intl} onChange={this.onFeaturePrivilegeChange} onChangeAll={this.onChangeAllFeaturePrivileges} spacesIndex={this.props.role.kibana.findIndex(k => isGlobalPrivilegeDefinition(k))} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts index d412ba63403e19..428836c9f181b9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawKibanaPrivileges } from '../../../../../../../../../common/model'; +import { RawKibanaPrivileges } from '../../../../../../../../common/model'; export const rawKibanaPrivileges: RawKibanaPrivileges = { global: { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index c20a391cdb20c5..e9f2f946e98859 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -351,108 +351,6 @@ exports[` renders without crashing 1`] = ` } disabled={true} features={Array []} - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } kibanaPrivileges={ KibanaPrivileges { "rawKibanaPrivileges": Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss index 5f9fbced5ee6ac..8f47727fdf8d62 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss @@ -9,6 +9,6 @@ .secPrivilegeMatrix__row--isBasePrivilege, .secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace, { +.secPrivilegeTable__row--isGlobalSpace { background-color: $euiColorLightestShade; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx index 62e22050132fd7..c6268e19abfd1f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx @@ -7,7 +7,7 @@ import { EuiIconTip, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PRIVILEGE_SOURCE } from '../../../../../../../lib/kibana_privilege_calculator'; +import { PRIVILEGE_SOURCE } from '../kibana_privilege_calculator'; import { PrivilegeDisplay } from './privilege_display'; describe('PrivilegeDisplay', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 6af7672f6fef86..55ac99da4c8c16 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -7,11 +7,8 @@ import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@el import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ReactNode, FC } from 'react'; -import { - PRIVILEGE_SOURCE, - PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from '../kibana_privilege_calculator'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props extends PropsOf { privilege: string | string[] | undefined; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx index ee121caa13a2af..16aad4826ae44c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx @@ -3,14 +3,14 @@ * 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 + import { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../..//lib/kibana_privilege_calculator'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeMatrix } from './privilege_matrix'; describe('PrivilegeMatrix', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index 962487312c83df..b3449e32c6c917 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -8,7 +8,6 @@ import { EuiButtonEmpty, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiModal, EuiModalBody, @@ -16,18 +15,16 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiOverlayMask, - // @ts-ignore - EuiToolTip, IconType, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { SpaceAvatar } from '../../../../../../../../../spaces/public/space_avatar'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, Role } from '../../../../../../../../common/model'; -import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; +import { SpaceAvatar } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, Role } from '../../../../../../../common/model'; +import { CalculatedPrivilege } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; @@ -258,11 +255,9 @@ export class PrivilegeMatrix extends Component { ]; return ( - // @ts-ignore missing rowProps from typedef { return { className: item.feature.isBase ? 'secPrivilegeMatrix__row--isBasePrivilege' : '', diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index 2b7d87f663d729..675f02a81f9e1d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -9,8 +9,8 @@ import { merge } from 'lodash'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { rawKibanaPrivileges } from './__fixtures__'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 5abb87d23bb6e6..6d1f5117c52e98 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -24,17 +24,16 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, copyRole } from '../../../../../../../common/model'; import { AllowedPrivilege, KibanaPrivilegeCalculatorFactory, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { hasAssignedFeaturePrivileges } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { hasAssignedFeaturePrivileges } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { SpaceSelector } from './space_selector'; @@ -285,7 +284,6 @@ export class PrivilegeSpaceForm extends Component { calculatedPrivileges={calculatedPrivileges} allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} - intl={this.props.intl} onChange={this.onFeaturePrivilegesChange} onChangeAll={this.onChangeAllFeaturePrivileges} kibanaPrivileges={this.props.kibanaPrivileges} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 37ee43c5473b0d..f0a391c98c9100 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -10,8 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { PrivilegeSpaceTable } from './privilege_space_table'; import { PrivilegeDisplay } from './privilege_display'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { rawKibanaPrivileges } from './__fixtures__'; interface TableRow { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 65a3df9fb47a1a..1c27ec84f50dcd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -12,22 +12,21 @@ import { EuiBasicTableColumn, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, Role, RoleKibanaPrivilege, -} from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition, hasAssignedFeaturePrivileges, -} from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx index 2756b1c4472744..e06d2a4f7dc337 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index d324cf99c8418a..21cadfafe1790b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -14,13 +14,12 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isReservedRole } from '../../../../../../../lib/role_utils'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, isReservedRole } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; @@ -34,7 +33,7 @@ interface Props { editable: boolean; validator: RoleValidator; intl: InjectedIntl; - uiCapabilities: UICapabilities; + uiCapabilities: Capabilities; features: Feature[]; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 0eb9cf0b0ee9d7..cfeb5b9f37d8ce 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -7,8 +7,8 @@ import { EuiComboBox, EuiComboBoxOptionProps, EuiHealth, EuiHighlight } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx index 9b483d92cde41b..d29b442420a90c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; -import { Role } from '../../../../../common/model'; +import { Role } from '../../../../common/model'; import { ReservedRoleBadge } from './reserved_role_badge'; const reservedRole: Role = { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx index 3d817d1e07d212..501ca7589dafde 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Role } from '../../../../../common/model'; -import { isReservedRole } from '../../../../lib/role_utils'; +import { Role, isReservedRole } from '../../../../common/model'; interface Props { role: Role; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss new file mode 100644 index 00000000000000..b40a32cb8df96b --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss @@ -0,0 +1 @@ +@import './spaces_popover_list'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index a99e389044eaad..bb7a6db97f7c88 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -14,9 +14,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { SpaceAvatar } from '../../../../../../../spaces/public/space_avatar'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../../../../plugins/spaces/common/constants'; -import { Space } from '../../../../../../../../../plugins/spaces/common/model/space'; +import { SpaceAvatar } from '../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../spaces/common'; +import { Space } from '../../../../../../spaces/common/model/space'; interface Props { spaces: Space[]; @@ -146,7 +146,6 @@ export class SpacesPopoverList extends Component { return (
{ - // @ts-ignore onSearch isn't defined on the type ({ + getFields: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.ts new file mode 100644 index 00000000000000..65d9a40a776eb0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.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 { HttpStart } from 'src/core/public'; + +export class IndicesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getFields(query: string) { + return ( + (await this.http.get(`/internal/security/fields/${encodeURIComponent(query)}`)) || + [] + ); + } +} diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts new file mode 100644 index 00000000000000..2564914a1d3d83 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const privilegesAPIClientMock = { + create: () => ({ + getAll: jest.fn(), + getBuiltIn: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts new file mode 100644 index 00000000000000..45bd2fd8fb3a6a --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { BuiltinESPrivileges, RawKibanaPrivileges } from '../../../common/model'; + +export class PrivilegesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getAll({ includeActions }: { includeActions: boolean }) { + return await this.http.get('/api/security/privileges', { + query: { includeActions }, + }); + } + + async getBuiltIn() { + return await this.http.get('/internal/security/esPrivileges/builtin'); + } +} diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts new file mode 100644 index 00000000000000..c4d3724c0ecb5f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const rolesAPIClientMock = { + create: () => ({ + getRoles: jest.fn(), + getRole: jest.fn(), + deleteRole: jest.fn(), + saveRole: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts rename to x-pack/plugins/security/public/management/roles/roles_api_client.test.ts index 1ea19f2637305e..75611613684051 100644 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts @@ -4,12 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { transformRoleForSave } from './transform_role_for_save'; +import { Role } from '../../../common/model'; +import { RolesAPIClient } from './roles_api_client'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('RolesAPIClient', () => { + async function saveRole(role: Role, spacesEnabled: boolean) { + const httpMock = httpServiceMock.createStartContract(); + const rolesAPIClient = new RolesAPIClient(httpMock); + + await rolesAPIClient.saveRole({ role, spacesEnabled }); + expect(httpMock.put).toHaveBeenCalledTimes(1); + + return JSON.parse(httpMock.put.mock.calls[0][1]?.body as any); + } -describe('transformRoleForSave', () => { describe('spaces disabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -20,7 +31,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -32,7 +43,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -43,7 +54,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -55,7 +66,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -74,7 +85,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ metadata: { @@ -89,7 +100,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -100,7 +111,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -112,7 +123,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -132,7 +143,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -150,7 +161,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -170,7 +181,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -191,7 +202,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove space privileges', () => { + it('should remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -219,7 +230,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -242,7 +253,7 @@ describe('transformRoleForSave', () => { }); describe('spaces enabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -253,7 +264,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -265,7 +276,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -276,7 +287,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -288,7 +299,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -307,7 +318,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ metadata: { @@ -322,7 +333,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -333,7 +344,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -345,7 +356,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -365,7 +376,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -383,7 +394,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -403,7 +414,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -424,7 +435,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove space privileges', () => { + it('should not remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -452,7 +463,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts new file mode 100644 index 00000000000000..d7e98e03a965b7 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.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 { HttpStart } from 'src/core/public'; +import { Role, RoleIndexPrivilege, copyRole } from '../../../common/model'; +import { isGlobalPrivilegeDefinition } from './edit_role/privilege_utils'; + +export class RolesAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getRoles() { + return await this.http.get('/api/security/role'); + } + + public async getRole(roleName: string) { + return await this.http.get(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async deleteRole(roleName: string) { + await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async saveRole({ role, spacesEnabled }: { role: Role; spacesEnabled: boolean }) { + await this.http.put(`/api/security/role/${encodeURIComponent(role.name)}`, { + body: JSON.stringify(this.transformRoleForSave(copyRole(role), spacesEnabled)), + }); + } + + private transformRoleForSave(role: Role, spacesEnabled: boolean) { + // Remove any placeholder index privileges + const isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => + indexPrivilege.names.length === 0; + role.elasticsearch.indices = role.elasticsearch.indices.filter( + indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) + ); + + // Remove any placeholder query entries + role.elasticsearch.indices.forEach(index => index.query || delete index.query); + + // If spaces are disabled, then do not persist any space privileges + if (!spacesEnabled) { + role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); + } + + role.kibana.forEach(kibanaPrivilege => { + // If a base privilege is defined, then do not persist feature privileges + if (kibanaPrivilege.base.length > 0) { + kibanaPrivilege.feature = {}; + } + }); + + delete role.name; + delete role.transient_metadata; + delete role._unrecognized_applications; + delete role._transform_error; + + return role; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index 34784b4b2accb6..37eed3357241d5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { Component, Fragment } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,23 +16,24 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component, Fragment } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { RolesApi } from '../../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { RolesAPIClient } from '../../roles_api_client'; interface Props { rolesToDelete: string[]; - intl: InjectedIntl; callback: (rolesToDelete: string[], errors: string[]) => void; onCancel: () => void; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { deleteInProgress: boolean; } -class ConfirmDeleteUI extends Component { +export class ConfirmDelete extends Component { constructor(props: Props) { super(props); this.state = { @@ -40,15 +42,12 @@ class ConfirmDeleteUI extends Component { } public render() { - const { rolesToDelete, intl } = this.props; + const { rolesToDelete } = this.props; const moreThanOne = rolesToDelete.length > 1; - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.deleteRoleTitle', - defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', - }, - { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` } - ); + const title = i18n.translate('xpack.security.management.roles.deleteRoleTitle', { + defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', + values: { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` }, + }); // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation @@ -128,32 +127,24 @@ class ConfirmDeleteUI extends Component { }; private deleteRoles = async () => { - const { rolesToDelete, callback } = this.props; + const { rolesToDelete, callback, rolesAPIClient, notifications } = this.props; const errors: string[] = []; const deleteOperations = rolesToDelete.map(roleName => { const deleteRoleTask = async () => { try { - await RolesApi.deleteRole(roleName); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role {roleName}', - }, - { roleName } + await rolesAPIClient.deleteRole(roleName); + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role {roleName}', values: { roleName } } ) ); } catch (e) { errors.push(roleName); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting role {roleName}', - }, - { roleName } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting role {roleName}', values: { roleName } } ) ); } @@ -167,5 +158,3 @@ class ConfirmDeleteUI extends Component { callback(rolesToDelete, errors); }; } - -export const ConfirmDelete = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 6da2f2442d488b..63ace53420612d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -4,50 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -let mockSimulate403 = false; -const mock403 = () => ({ body: { statusCode: 403 } }); -jest.mock('../../../../lib/roles_api', () => { - return { - RolesApi: { - async getRoles() { - if (mockSimulate403) { - throw mock403(); - } - return [ - { - name: 'test-role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - }, - { - name: 'reserved-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - metadata: { - _reserved: true, - }, - }, - { - name: 'disabled-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - transient_metadata: { - enabled: false, - }, - }, - ]; - }, - }, - }; -}); - import { EuiIcon } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { RolesAPIClient } from '../roles_api_client'; import { PermissionDenied } from './permission_denied'; import { RolesGridPage } from './roles_grid_page'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { rolesAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); + const waitForRender = async ( wrapper: ReactWrapper, condition: (wrapper: ReactWrapper) => boolean @@ -69,12 +38,37 @@ const waitForRender = async ( }; describe('', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; + apiClientMock = rolesAPIClientMock.create(); + apiClientMock.getRoles.mockResolvedValue([ + { + name: 'test-role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, + { + name: 'reserved-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + metadata: { _reserved: true }, + }, + { + name: 'disabled-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, + }, + ]); }); it(`renders reserved roles as such`, async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { @@ -87,8 +81,14 @@ describe('', () => { }); it('renders permission denied if required', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.getRoles.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; }); @@ -96,7 +96,12 @@ describe('', () => { }); it('renders role actions as appropriate', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 2083a93f4b33c9..7c686bef391a79 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { EuiButton, EuiIcon, @@ -18,18 +20,17 @@ import { EuiButtonIcon, EuiBasicTableColumn, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { Role } from '../../../../../common/model'; -import { isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../lib/role_utils'; -import { RolesApi } from '../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { Role, isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../common/model'; +import { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; interface Props { - intl: InjectedIntl; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { @@ -44,7 +45,7 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `#/management/security/roles/${action}${roleName ? `/${roleName}` : ''}`; }; -class RolesGridPageUI extends Component { +export class RolesGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -68,7 +69,6 @@ class RolesGridPageUI extends Component { private getPageContent = () => { const { roles } = this.state; - const { intl } = this.props; return ( @@ -105,6 +105,8 @@ class RolesGridPageUI extends Component { onCancel={this.onCancelDelete} rolesToDelete={this.state.selection.map(role => role.name)} callback={this.handleDelete} + notifications={this.props.notifications} + rolesAPIClient={this.props.rolesAPIClient} /> ) : null} @@ -112,7 +114,7 @@ class RolesGridPageUI extends Component { !role.metadata || !role.metadata._reserved, @@ -155,17 +157,16 @@ class RolesGridPageUI extends Component { ); }; - private getColumnConfig = (intl: InjectedIntl) => { - const reservedRoleDesc = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnDescription', - defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.', - }); + private getColumnConfig = () => { + const reservedRoleDesc = i18n.translate( + 'xpack.security.management.roles.reservedColumnDescription', + { defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.' } + ); return [ { field: 'name', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.nameColumnName', + name: i18n.translate('xpack.security.management.roles.nameColumnName', { defaultMessage: 'Role', }), sortable: true, @@ -188,8 +189,7 @@ class RolesGridPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnName', + name: i18n.translate('xpack.security.management.roles.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: Role) => Boolean(metadata && metadata._reserved), @@ -197,8 +197,7 @@ class RolesGridPageUI extends Component { align: 'right', description: reservedRoleDesc, render: (metadata: Role['metadata']) => { - const label = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedRoleIconLabel', + const label = i18n.translate('xpack.security.management.roles.reservedRoleIconLabel', { defaultMessage: 'Reserved role', }); @@ -210,8 +209,7 @@ class RolesGridPageUI extends Component { }, }, { - name: intl.formatMessage({ - id: 'xpack.security.management.roles.actionsColumnName', + name: i18n.translate('xpack.security.management.roles.actionsColumnName', { defaultMessage: 'Actions', }), width: '150px', @@ -219,15 +217,10 @@ class RolesGridPageUI extends Component { { available: (role: Role) => !isReadOnlyRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.editRoleActionName', - defaultMessage: `Edit {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { + defaultMessage: `Edit {roleName}`, + values: { roleName: role.name }, + }); return ( { { available: (role: Role) => !isReservedRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.cloneRoleActionName', - defaultMessage: `Clone {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { + defaultMessage: `Clone {roleName}`, + values: { roleName: role.name }, + }); return ( { private async loadRoles() { try { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } catch (e) { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.roles.fetchingRolesErrorMessage', - defaultMessage: 'Error fetching roles: {message}', - }, - { message: _.get(e, 'body.message', '') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -339,5 +324,3 @@ class RolesGridPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const RolesGridPage = injectI18n(RolesGridPageUI); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx new file mode 100644 index 00000000000000..48bc1a6580a93d --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { licenseMock } from '../../../common/licensing/index.mock'; + +jest.mock('./roles_grid', () => ({ + RolesGridPage: (props: any) => `Roles Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role', () => ({ + EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`, +})); + +import { rolesManagementApp } from './roles_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const { fatalErrors } = coreMock.createSetup(); + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await rolesManagementApp + .create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: jest.fn().mockResolvedValue([coreMock.createStart(), { data: {} }]), + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('rolesManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + expect( + rolesManagementApp.create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "roles", + "mount": [Function], + "order": 20, + "title": "Roles", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Roles' }]); + expect(container).toMatchInlineSnapshot(` +
+ Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { href: `#/some-base-path/roles/edit/${roleName}`, text: roleName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `clone role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/clone/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role name in `edit role` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'some 安全性 role'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { + href: '#/some-base-path/roles/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', + text: roleName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx new file mode 100644 index 00000000000000..4e8c95b61c2f19 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../../common/licensing'; +import { PluginStartDependencies } from '../../plugin'; +import { UserAPIClient } from '../users'; +import { RolesAPIClient } from './roles_api_client'; +import { RolesGridPage } from './roles_grid'; +import { EditRolePage } from './edit_role'; +import { DocumentationLinksService } from './documentation_links'; +import { IndicesAPIClient } from './indices_api_client'; +import { PrivilegesAPIClient } from './privileges_api_client'; + +interface CreateParams { + fatalErrors: FatalErrorsSetup; + license: SecurityLicense; + getStartServices: CoreSetup['getStartServices']; +} + +export const rolesManagementApp = Object.freeze({ + id: 'roles', + create({ license, fatalErrors, getStartServices }: CreateParams) { + return { + id: this.id, + order: 20, + title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data }, + ] = await getStartServices(); + + const rolesBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), + href: `#${basePath}`, + }, + ]; + + const rolesAPIClient = new RolesAPIClient(http); + const RolesGridPageWithBreadcrumbs = () => { + setBreadcrumbs(rolesBreadcrumbs); + return ; + }; + + const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { + const { roleName } = useParams<{ roleName?: string }>(); + + setBreadcrumbs([ + ...rolesBreadcrumbs, + action === 'edit' && roleName + ? { text: roleName, href: `#${basePath}/edit/${encodeURIComponent(roleName)}` } + : { + text: i18n.translate('xpack.security.roles.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss new file mode 100644 index 00000000000000..35df0c1b965837 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/_index.scss @@ -0,0 +1 @@ +@import './edit_user/index'; diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx index 221120532318cb..be46612767a593 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx @@ -7,10 +7,12 @@ import { EuiFieldText } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { User } from '../../../../../common/model'; import { ChangePasswordForm } from './change_password_form'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; + function getCurrentPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]'); } @@ -23,8 +25,6 @@ function getConfirmPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]'); } -jest.mock('ui/kfetch'); - describe('', () => { describe('for the current user', () => { it('shows fields for current and new passwords', () => { @@ -40,7 +40,8 @@ describe('', () => { ); @@ -60,15 +61,15 @@ describe('', () => { const callback = jest.fn(); - const apiClient = new UserAPIClient(); - apiClient.changePassword = jest.fn(); + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); @@ -83,8 +84,8 @@ describe('', () => { wrapper.find('button[data-test-subj="changePasswordButton"]').simulate('click'); - expect(apiClient.changePassword).toHaveBeenCalledTimes(1); - expect(apiClient.changePassword).toHaveBeenCalledWith( + expect(apiClientMock.changePassword).toHaveBeenCalledTimes(1); + expect(apiClientMock.changePassword).toHaveBeenCalledWith( 'user', 'myNewPassword', 'myCurrentPassword' @@ -106,7 +107,8 @@ describe('', () => { ); diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 61c0b77decd565..6dcf330ec6f9e1 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -5,10 +5,7 @@ */ import { EuiButton, - // @ts-ignore EuiButtonEmpty, - // @ts-ignore - EuiDescribedFormGroup, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -18,15 +15,16 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../../common/model'; +import { UserAPIClient } from '../..'; interface Props { user: User; isUserChangingOwnPassword: boolean; onChangePassword?: () => void; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -294,7 +292,7 @@ export class ChangePasswordForm extends Component { }; private handleChangePasswordSuccess = () => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.account.changePasswordSuccess', { defaultMessage: 'Your password has been changed.', }), @@ -317,7 +315,7 @@ export class ChangePasswordForm extends Component { if (error.body && error.body.statusCode === 403) { this.setState({ currentPasswordError: true }); } else { - toastNotifications.addDanger( + this.props.notifications.toasts.addDanger( i18n.translate('xpack.security.management.users.editUser.settingPasswordErrorMessage', { defaultMessage: 'Error setting password: {message}', values: { message: _.get(error, 'body.message') }, diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts b/x-pack/plugins/security/public/management/users/components/change_password_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts rename to x-pack/plugins/security/public/management/users/components/change_password_form/index.ts diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx similarity index 58% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx index 9f69fc7a7551f0..bcec707b03f936 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx @@ -5,16 +5,21 @@ */ import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ConfirmDeleteUsers } from './confirm_delete'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import React from 'react'; -import { UserAPIClient } from '../../../lib/api'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; describe('ConfirmDeleteUsers', () => { it('renders a warning for a single user', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find('EuiModalHeaderTitle').text()).toMatchInlineSnapshot(`"Delete user foo"`); @@ -23,7 +28,8 @@ describe('ConfirmDeleteUsers', () => { it('renders a warning for a multiple users', () => { const wrapper = mountWithIntl( @@ -35,7 +41,12 @@ describe('ConfirmDeleteUsers', () => { it('fires onCancel when the operation is cancelled', () => { const onCancel = jest.fn(); const wrapper = mountWithIntl( - + ); expect(onCancel).toBeCalledTimes(0); @@ -47,50 +58,48 @@ describe('ConfirmDeleteUsers', () => { it('deletes the requested users when confirmed', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn(); - - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); it('attempts to delete all users even if some fail', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn().mockImplementation(user => { + + const apiClientMock = userAPIClientMock.create(); + apiClientMock.deleteUser.mockImplementation(user => { if (user === 'foo') { return Promise.reject('something terrible happened'); } return Promise.resolve(); }); - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; - const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); }); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx similarity index 50% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index 53bb022afb513f..b7269e0168d7d5 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -6,51 +6,46 @@ import React, { Component, Fragment } from 'react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { UserAPIClient } from '../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { UserAPIClient } from '../..'; interface Props { - intl: InjectedIntl; usersToDelete: string[]; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; onCancel: () => void; callback?: (usersToDelete: string[], errors: string[]) => void; } -class ConfirmDeleteUI extends Component { +export class ConfirmDeleteUsers extends Component { public render() { - const { usersToDelete, onCancel, intl } = this.props; + const { usersToDelete, onCancel } = this.props; const moreThanOne = usersToDelete.length > 1; const title = moreThanOne - ? intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', - defaultMessage: 'Delete {userLength} users', - }, - { userLength: usersToDelete.length } - ) - : intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteOneUserTitle', - defaultMessage: 'Delete user {userLength}', - }, - { userLength: usersToDelete[0] } - ); + ? i18n.translate('xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', { + defaultMessage: 'Delete {userLength} users', + values: { userLength: usersToDelete.length }, + }) + : i18n.translate('xpack.security.management.users.confirmDelete.deleteOneUserTitle', { + defaultMessage: 'Delete user {userLength}', + values: { userLength: usersToDelete[0] }, + }); return (
@@ -82,31 +77,23 @@ class ConfirmDeleteUI extends Component { } private deleteUsers = () => { - const { usersToDelete, callback, apiClient } = this.props; + const { usersToDelete, callback, apiClient, notifications } = this.props; const errors: string[] = []; usersToDelete.forEach(async username => { try { await apiClient.deleteUser(username); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted user {username}', - }, - { username } + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted user {username}', values: { username } } ) ); } catch (e) { errors.push(username); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting user {username}', - }, - { username } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting user {username}', values: { username } } ) ); } @@ -116,5 +103,3 @@ class ConfirmDeleteUI extends Component { }); }; } - -export const ConfirmDeleteUsers = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/index.ts b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts similarity index 79% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/index.ts rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts index 9f4d4239d6b4cc..fde35ab0f0d02e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/index.ts +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ApiKeysGridPage } from './api_keys_grid_page'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/plugins/security/public/management/users/components/index.ts b/x-pack/plugins/security/public/management/users/components/index.ts new file mode 100644 index 00000000000000..54011a6a24cbd2 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ChangePasswordForm } from './change_password_form'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss b/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss rename to x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss new file mode 100644 index 00000000000000..734ba7882ba72c --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/_index.scss @@ -0,0 +1 @@ +@import './edit_user_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx similarity index 70% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 639646ce48e224..543d20bb92afe8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -8,13 +8,14 @@ import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { securityMock } from '../../../../../../../../plugins/security/public/mocks'; -import { UserAPIClient } from '../../../../lib/api'; -import { User, Role } from '../../../../../common/model'; +import { User, Role } from '../../../../common/model'; import { ReactWrapper } from 'enzyme'; -import { mockAuthenticatedUser } from '../../../../../../../../plugins/security/common/model/authenticated_user.mock'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../../mocks'; +import { rolesAPIClientMock } from '../../roles/index.mock'; +import { userAPIClientMock } from '../index.mock'; const createUser = (username: string) => { const user: User = { @@ -34,14 +35,12 @@ const createUser = (username: string) => { return user; }; -const buildClient = () => { - const apiClient = new UserAPIClient(); +const buildClients = () => { + const apiClient = userAPIClientMock.create(); + apiClient.getUser.mockImplementation(async (username: string) => createUser(username)); - apiClient.getUser = jest - .fn() - .mockImplementation(async (username: string) => createUser(username)); - - apiClient.getRoles = jest.fn().mockImplementation(() => { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRoles.mockImplementation(() => { return Promise.resolve([ { name: 'role 1', @@ -64,7 +63,7 @@ const buildClient = () => { ] as Role[]); }); - return apiClient; + return { apiClient, rolesAPIClient }; }; function buildSecuritySetup() { @@ -85,15 +84,15 @@ function expectMissingSaveButton(wrapper: ReactWrapper) { describe('EditUserPage', () => { it('allows reserved users to be viewed', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -106,15 +105,15 @@ describe('EditUserPage', () => { }); it('allows new users to be created', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -127,15 +126,15 @@ describe('EditUserPage', () => { }); it('allows existing users to be edited', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx similarity index 77% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index bbffe07485f8dc..576f3ff9e6008c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -26,22 +26,23 @@ import { EuiHorizontalRule, EuiSpacer, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { SecurityPluginSetup } from '../../../../../../../../plugins/security/public'; -import { UserValidator, UserValidationResult } from '../../../../lib/validate_user'; -import { User, EditUser, Role } from '../../../../../common/model'; -import { USERS_PATH } from '../../../../views/management/management_urls'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { UserAPIClient } from '../../../../lib/api'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User, EditUser, Role } from '../../../../common/model'; +import { AuthenticationServiceSetup } from '../../../authentication'; +import { USERS_PATH } from '../../management_urls'; +import { RolesAPIClient } from '../../roles'; +import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; +import { UserValidator, UserValidationResult } from './validate_user'; +import { UserAPIClient } from '..'; interface Props { - username: string; - intl: InjectedIntl; - changeUrl: (path: string) => void; - apiClient: UserAPIClient; - securitySetup: SecurityPluginSetup; + username?: string; + apiClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + authc: AuthenticationServiceSetup; + notifications: NotificationsStart; } interface State { @@ -56,7 +57,11 @@ interface State { formError: UserValidationResult | null; } -class EditUserPageUI extends Component { +function backToUserList() { + window.location.hash = USERS_PATH; +} + +export class EditUserPage extends Component { private validator: UserValidator; constructor(props: Props) { @@ -84,7 +89,17 @@ class EditUserPageUI extends Component { } public async componentDidMount() { - const { username, apiClient, securitySetup } = this.props; + await this.setCurrentUser(); + } + + public async componentDidUpdate(prevProps: Props) { + if (prevProps.username !== this.props.username) { + await this.setCurrentUser(); + } + } + + private async setCurrentUser() { + const { username, apiClient, rolesAPIClient, notifications, authc } = this.props; let { user, currentUser } = this.state; if (username) { try { @@ -93,26 +108,24 @@ class EditUserPageUI extends Component { password: '', confirmPassword: '', }; - currentUser = await securitySetup.authc.getCurrentUser(); + currentUser = await authc.getCurrentUser(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingUserTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { defaultMessage: 'Error loading user', }), text: get(err, 'body.message') || err.message, }); - return; + return backToUserList(); } } let roles: Role[] = []; try { - roles = await apiClient.getRoles(); + roles = await rolesAPIClient.getRoles(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingRolesTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { defaultMessage: 'Error loading roles', }), text: get(err, 'body.message') || err.message, @@ -131,8 +144,7 @@ class EditUserPageUI extends Component { private handleDelete = (usernames: string[], errors: string[]) => { if (errors.length === 0) { - const { changeUrl } = this.props; - changeUrl(USERS_PATH); + backToUserList(); } }; @@ -148,7 +160,7 @@ class EditUserPageUI extends Component { this.setState({ formError: null, }); - const { changeUrl, apiClient } = this.props; + const { apiClient } = this.props; const { user, isNewUser, selectedRoles } = this.state; const userToSave: EditUser = { ...user }; if (!isNewUser) { @@ -160,26 +172,23 @@ class EditUserPageUI extends Component { }); try { await apiClient.saveUser(userToSave); - toastNotifications.addSuccess( - this.props.intl.formatMessage( + this.props.notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', { - id: - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', defaultMessage: 'Saved user {message}', - }, - { message: user.username } + values: { message: user.username }, + } ) ); - changeUrl(USERS_PATH); + + backToUserList(); } catch (e) { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.editUser.savingUserErrorMessage', - defaultMessage: 'Error saving user: {message}', - }, - { message: get(e, 'body.message', 'Unknown error') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { + defaultMessage: 'Error saving user: {message}', + values: { message: get(e, 'body.message', 'Unknown error') }, + }) ); } } @@ -189,8 +198,7 @@ class EditUserPageUI extends Component { return ( { /> { {user.username === 'kibana' ? ( @@ -260,6 +268,7 @@ class EditUserPageUI extends Component { isUserChangingOwnPassword={userIsLoggedInUser} onChangePassword={this.toggleChangePasswordForm} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ); @@ -352,7 +361,6 @@ class EditUserPageUI extends Component { }; public render() { - const { changeUrl, intl } = this.props; const { user, roles, @@ -417,6 +425,7 @@ class EditUserPageUI extends Component { usersToDelete={[user.username]} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -425,17 +434,16 @@ class EditUserPageUI extends Component { {...this.validator.validateUsername(this.state.user)} helpText={ !isNewUser && !reserved - ? intl.formatMessage({ - id: - 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', - defaultMessage: `Usernames can't be changed after creation.`, - }) + ? i18n.translate( + 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', + { defaultMessage: `Usernames can't be changed after creation.` } + ) : null } - label={intl.formatMessage({ - id: 'xpack.security.management.users.editUser.usernameFormRowLabel', - defaultMessage: 'Username', - })} + label={i18n.translate( + 'xpack.security.management.users.editUser.usernameFormRowLabel', + { defaultMessage: 'Username' } + )} > { {reserved ? null : ( { { )} { @@ -513,7 +521,7 @@ class EditUserPageUI extends Component { {reserved && ( - changeUrl(USERS_PATH)}> + { - changeUrl(USERS_PATH)} - > + { ); } } - -export const EditUserPage = injectI18n(EditUserPageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts rename to x-pack/plugins/security/public/management/users/edit_user/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.test.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts index 0535248fede88d..6050e1868a759a 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts @@ -5,7 +5,7 @@ */ import { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; function expectValid(result: UserValidationResult) { expect(result.isInvalid).toBe(false); diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.ts index 113aaacdcbf966..5edd96c68bf0dc 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; interface UserValidatorOptions { shouldValidate?: boolean; diff --git a/x-pack/legacy/plugins/security/public/views/management/index.js b/x-pack/plugins/security/public/management/users/index.mock.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/views/management/index.js rename to x-pack/plugins/security/public/management/users/index.mock.ts index 0ed6fe09ef80a4..f090f88da500de 100644 --- a/x-pack/legacy/plugins/security/public/views/management/index.js +++ b/x-pack/plugins/security/public/management/users/index.mock.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './management'; +export { userAPIClientMock } from './user_api_client.mock'; diff --git a/x-pack/legacy/plugins/security/public/objects/index.ts b/x-pack/plugins/security/public/management/users/index.ts similarity index 68% rename from x-pack/legacy/plugins/security/public/objects/index.ts rename to x-pack/plugins/security/public/management/users/index.ts index a6238ca879901c..c8b4d41973da6a 100644 --- a/x-pack/legacy/plugins/security/public/objects/index.ts +++ b/x-pack/plugins/security/public/management/users/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { saveRole, deleteRole } from './lib/roles'; - -export { getFields } from './lib/get_fields'; +export { UserAPIClient } from './user_api_client'; +export { usersManagementApp } from './users_management_app'; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts new file mode 100644 index 00000000000000..7223f78d57fdc0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const userAPIClientMock = { + create: () => ({ + getUsers: jest.fn(), + getUser: jest.fn(), + deleteUser: jest.fn(), + saveUser: jest.fn(), + changePassword: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.ts b/x-pack/plugins/security/public/management/users/user_api_client.ts new file mode 100644 index 00000000000000..61dd09d2c5e3de --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { User, EditUser } from '../../../common/model'; + +const usersUrl = '/internal/security/users'; + +export class UserAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getUsers() { + return await this.http.get(usersUrl); + } + + public async getUser(username: string) { + return await this.http.get(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async deleteUser(username: string) { + await this.http.delete(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async saveUser(user: EditUser) { + await this.http.post(`${usersUrl}/${encodeURIComponent(user.username)}`, { + body: JSON.stringify(user), + }); + } + + public async changePassword(username: string, password: string, currentPassword: string) { + const data: Record = { + newPassword: password, + }; + if (currentPassword) { + data.password = currentPassword; + } + + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/password`, { + body: JSON.stringify(data), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts b/x-pack/plugins/security/public/management/users/users_grid/index.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts rename to x-pack/plugins/security/public/management/users/users_grid/index.ts index 03721f5ce93b1f..90e16248e19c36 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts +++ b/x-pack/plugins/security/public/management/users/users_grid/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UsersListPage } from './users_list_page'; +export { UsersGridPage } from './users_grid_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx index bdc0df9bae67c3..def0649953437d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UserAPIClient } from '../../../../lib/api'; -import { User } from '../../../../../common/model'; +import { User } from '../../../../common/model'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UsersListPage } from './users_list_page'; +import { UsersGridPage } from './users_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; +import { userAPIClientMock } from '../index.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; -jest.mock('ui/kfetch'); - -describe('UsersListPage', () => { +describe('UsersGridPage', () => { it('renders the list of users', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { return Promise.resolve([ { username: 'foo', @@ -38,22 +37,27 @@ describe('UsersListPage', () => { ]); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); - expect(apiClient.getUsers).toBeCalledTimes(1); + expect(apiClientMock.getUsers).toBeCalledTimes(1); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); it('renders a forbidden message if user is not authorized', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { - return Promise.reject({ body: { statusCode: 403 } }); - }); + const apiClient = userAPIClientMock.create(); + apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx similarity index 85% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index df8522e5f32f91..fa15c3388fcc99 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -19,15 +19,16 @@ import { EuiEmptyPrompt, EuiBasicTableColumn, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { User } from '../../../../../common/model'; -import { UserAPIClient } from '../../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../common/model'; +import { ConfirmDeleteUsers } from '../components'; +import { UserAPIClient } from '..'; interface Props { - intl: InjectedIntl; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -38,7 +39,7 @@ interface State { filter: string; } -class UsersListPageUI extends Component { +export class UsersGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -56,7 +57,6 @@ class UsersListPageUI extends Component { public render() { const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state; - const { intl } = this.props; if (permissionDenied) { return ( @@ -88,8 +88,7 @@ class UsersListPageUI extends Component { const columns: Array> = [ { field: 'full_name', - name: intl.formatMessage({ - id: 'xpack.security.management.users.fullNameColumnName', + name: i18n.translate('xpack.security.management.users.fullNameColumnName', { defaultMessage: 'Full Name', }), sortable: true, @@ -100,8 +99,7 @@ class UsersListPageUI extends Component { }, { field: 'username', - name: intl.formatMessage({ - id: 'xpack.security.management.users.userNameColumnName', + name: i18n.translate('xpack.security.management.users.userNameColumnName', { defaultMessage: 'User Name', }), sortable: true, @@ -114,8 +112,7 @@ class UsersListPageUI extends Component { }, { field: 'email', - name: intl.formatMessage({ - id: 'xpack.security.management.users.emailAddressColumnName', + name: i18n.translate('xpack.security.management.users.emailAddressColumnName', { defaultMessage: 'Email Address', }), sortable: true, @@ -126,8 +123,7 @@ class UsersListPageUI extends Component { }, { field: 'roles', - name: intl.formatMessage({ - id: 'xpack.security.management.users.rolesColumnName', + name: i18n.translate('xpack.security.management.users.rolesColumnName', { defaultMessage: 'Roles', }), render: (rolenames: string[]) => { @@ -144,15 +140,13 @@ class UsersListPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnName', + name: i18n.translate('xpack.security.management.users.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: User) => Boolean(metadata && metadata._reserved), width: '100px', align: 'right', - description: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnDescription', + description: i18n.translate('xpack.security.management.users.reservedColumnDescription', { defaultMessage: 'Reserved users are built-in and cannot be removed. Only the password can be changed.', }), @@ -233,6 +227,7 @@ class UsersListPageUI extends Component { usersToDelete={selection.map(user => user.username)} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -275,14 +270,11 @@ class UsersListPageUI extends Component { if (e.body.statusCode === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.fetchingUsersErrorMessage', - defaultMessage: 'Error fetching users: {message}', - }, - { message: e.body.message } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.fetchingUsersErrorMessage', { + defaultMessage: 'Error fetching users: {message}', + values: { message: e.body.message }, + }) ); } } @@ -315,5 +307,3 @@ class UsersListPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const UsersListPage = injectI18n(UsersListPageUI); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx new file mode 100644 index 00000000000000..48ffcfc550a849 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('./users_grid', () => ({ + UsersGridPage: (props: any) => `Users Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_user', () => ({ + EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, +})); + +import { usersManagementApp } from './users_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { securityMock } from '../../mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await usersManagementApp + .create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('usersManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + usersManagementApp.create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "users", + "mount": [Function], + "order": 10, + "title": "Users", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]); + expect(container).toMatchInlineSnapshot(` +
+ Users Page: {"notifications":{"toasts":{}},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create user` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit user` page', async () => { + const basePath = '/some-base-path/users'; + const userName = 'someUserName'; + window.location.hash = `${basePath}/edit/${userName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { href: `#/some-base-path/users/edit/${userName}`, text: userName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/users'; + const username = 'some 安全性 user'; + window.location.hash = `${basePath}/edit/${username}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { + href: '#/some-base-path/users/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', + text: username, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx new file mode 100644 index 00000000000000..9aebb396ce9a92 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { AuthenticationServiceSetup } from '../../authentication'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { UserAPIClient } from './user_api_client'; +import { UsersGridPage } from './users_grid'; +import { EditUserPage } from './edit_user'; + +interface CreateParams { + authc: AuthenticationServiceSetup; + getStartServices: CoreSetup['getStartServices']; +} + +export const usersManagementApp = Object.freeze({ + id: 'users', + create({ authc, getStartServices }: CreateParams) { + return { + id: this.id, + order: 10, + title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); + const usersBreadcrumbs = [ + { + text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), + href: `#${basePath}`, + }, + ]; + + const userAPIClient = new UserAPIClient(http); + const UsersGridPageWithBreadcrumbs = () => { + setBreadcrumbs(usersBreadcrumbs); + return ; + }; + + const EditUserPageWithBreadcrumbs = () => { + const { username } = useParams<{ username?: string }>(); + + setBreadcrumbs([ + ...usersBreadcrumbs, + username + ? { text: username, href: `#${basePath}/edit/${encodeURIComponent(username)}` } + : { + text: i18n.translate('xpack.security.users.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts deleted file mode 100644 index 50e0b838c750fc..00000000000000 --- a/x-pack/plugins/security/public/plugin.ts +++ /dev/null @@ -1,74 +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 { Plugin, CoreSetup, CoreStart } from 'src/core/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { - SessionExpired, - SessionTimeout, - ISessionTimeout, - SessionTimeoutHttpInterceptor, - UnauthorizedResponseHttpInterceptor, -} from './session'; -import { SecurityLicenseService } from '../common/licensing'; -import { SecurityNavControlService } from './nav_control'; -import { AuthenticationService } from './authentication'; - -export interface PluginSetupDependencies { - licensing: LicensingPluginSetup; -} - -export class SecurityPlugin implements Plugin { - private sessionTimeout!: ISessionTimeout; - - private navControlService!: SecurityNavControlService; - - private securityLicenseService!: SecurityLicenseService; - - public setup(core: CoreSetup, { licensing }: PluginSetupDependencies) { - const { http, notifications, injectedMetadata } = core; - const { basePath, anonymousPaths } = http; - anonymousPaths.register('/login'); - anonymousPaths.register('/logout'); - anonymousPaths.register('/logged_out'); - - const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; - const sessionExpired = new SessionExpired(basePath, tenant); - http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); - this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); - - this.navControlService = new SecurityNavControlService(); - this.securityLicenseService = new SecurityLicenseService(); - const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); - - const authc = new AuthenticationService().setup({ http: core.http }); - - this.navControlService.setup({ - securityLicense: license, - authc, - }); - - return { - authc, - sessionTimeout: this.sessionTimeout, - }; - } - - public start(core: CoreStart) { - this.sessionTimeout.start(); - this.navControlService.start({ core }); - } - - public stop() { - this.sessionTimeout.stop(); - this.navControlService.stop(); - this.securityLicenseService.stop(); - } -} - -export type SecurityPluginSetup = ReturnType; -export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx new file mode 100644 index 00000000000000..394e23cbbf646c --- /dev/null +++ b/x-pack/plugins/security/public/plugin.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { Plugin, CoreSetup, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { + SessionExpired, + SessionTimeout, + ISessionTimeout, + SessionTimeoutHttpInterceptor, + UnauthorizedResponseHttpInterceptor, +} from './session'; +import { SecurityLicenseService } from '../common/licensing'; +import { SecurityNavControlService } from './nav_control'; +import { AccountManagementPage } from './account_management'; +import { AuthenticationService, AuthenticationServiceSetup } from './authentication'; +import { ManagementService, UserAPIClient } from './management'; + +export interface PluginSetupDependencies { + licensing: LicensingPluginSetup; + home?: HomePublicPluginSetup; + management?: ManagementSetup; +} + +export interface PluginStartDependencies { + data: DataPublicPluginStart; + management?: ManagementStart; +} + +export class SecurityPlugin + implements + Plugin< + SecurityPluginSetup, + SecurityPluginStart, + PluginSetupDependencies, + PluginStartDependencies + > { + private sessionTimeout!: ISessionTimeout; + private readonly navControlService = new SecurityNavControlService(); + private readonly securityLicenseService = new SecurityLicenseService(); + private readonly managementService = new ManagementService(); + private authc!: AuthenticationServiceSetup; + + public setup( + core: CoreSetup, + { home, licensing, management }: PluginSetupDependencies + ) { + const { http, notifications, injectedMetadata } = core; + const { basePath, anonymousPaths } = http; + anonymousPaths.register('/login'); + anonymousPaths.register('/logout'); + anonymousPaths.register('/logged_out'); + + const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; + const sessionExpired = new SessionExpired(basePath, tenant); + http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); + this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); + http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); + + const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); + + this.authc = new AuthenticationService().setup({ http: core.http }); + + this.navControlService.setup({ + securityLicense: license, + authc: this.authc, + }); + + if (management) { + this.managementService.setup({ + license, + management, + authc: this.authc, + fatalErrors: core.fatalErrors, + getStartServices: core.getStartServices, + }); + } + + if (management && home) { + home.featureCatalogue.register({ + id: 'security', + title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { + defaultMessage: 'Security Settings', + }), + description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { + defaultMessage: + 'Protect your data and easily manage who has access to what with users and roles.', + }), + icon: 'securityApp', + path: '/app/kibana#/management/security/users', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + return { + authc: this.authc, + sessionTimeout: this.sessionTimeout, + }; + } + + public start(core: CoreStart, { data, management }: PluginStartDependencies) { + this.sessionTimeout.start(); + this.navControlService.start({ core }); + + if (management) { + this.managementService.start({ management }); + } + + return { + __legacyCompat: { + account_management: { + AccountManagementPage: () => ( + + + + ), + }, + }, + }; + } + + public stop() { + this.sessionTimeout.stop(); + this.navControlService.stop(); + this.securityLicenseService.stop(); + this.managementService.stop(); + } +} + +export type SecurityPluginSetup = ReturnType; +export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 5e32a0e90198a6..56aad4ece3e958 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import { ICustomClusterClient, CoreSetup } from '../../../../src/core/server'; +import { ICustomClusterClient } from '../../../../src/core/server'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { Plugin, PluginSetupDependencies } from './plugin'; @@ -14,7 +14,7 @@ import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/ describe('Security Plugin', () => { let plugin: Plugin; - let mockCoreSetup: MockedKeys; + let mockCoreSetup: ReturnType; let mockClusterClient: jest.Mocked; let mockDependencies: PluginSetupDependencies; beforeEach(() => { diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 9cd5cf83092e18..def6fabc0e3222 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; -import { RoleMapping } from '../../../../../legacy/plugins/security/common/model'; +import { RoleMapping } from '../../../common/model'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 65baa1bd991022..c1f0f8bd3ece41 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -5,5 +5,5 @@ */ export { isReservedSpace } from './is_reserved_space'; -export { MAX_SPACE_INITIALS } from './constants'; +export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index 93e98f33a30b04..da9640fa3e0718 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -401,7 +401,7 @@ export async function claimAvailableTasks( } else { performance.mark('claimAvailableTasks.noAvailableWorkers'); logger.info( - `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers. If this happens often, consider adjusting the "xpack.task_manager.max_workers" configuration.` + `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers.` ); } return []; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 84020926955906..f61dfa8d886c28 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -77,8 +77,6 @@ }, "messages": { "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", - "common.ui.aggResponse.fieldLabel": "フィールド", - "common.ui.aggResponse.valueLabel": "値", "common.ui.aggTypes.aggNotValidLabel": "- 無効な集約 -", "common.ui.aggTypes.aggregateWith.noAggsErrorTooltip": "選択されたフィールドには互換性のある集約がありません。", "common.ui.aggTypes.aggregateWithLabel": "アグリゲーション:", @@ -452,7 +450,6 @@ "common.ui.flotCharts.thuLabel": "木", "common.ui.flotCharts.tueLabel": "火", "common.ui.flotCharts.wedLabel": "水", - "common.ui.management.breadcrumb": "管理", "common.ui.modals.cancelButtonLabel": "キャンセル", "common.ui.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", "common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP リクエストが接続に失敗しました。Kibana サーバーが実行されていて、ご使用のブラウザの接続が正常に動作していることを確認するか、システム管理者にお問い合わせください。", @@ -503,27 +500,12 @@ "common.ui.vis.editors.aggGroups.bucketsText": "バケット", "common.ui.vis.editors.aggGroups.metricsText": "メトリック", "common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage": "「{schema}」集約は他のバケットの前に実行する必要があります!", - "common.ui.vis.editors.resizeAriaLabels": "左右のキーでエディターのサイズを変更します", - "common.ui.vis.editors.sidebar.applyChangesAriaLabel": "ビジュアライゼーションを変更と共に更新します", - "common.ui.vis.editors.sidebar.applyChangesTooltip": "変更を適用", "common.ui.vis.editors.sidebar.autoApplyChangesAriaLabel": "変更されるごとにビジュアライゼーションを自動的に更新します", - "common.ui.vis.editors.sidebar.autoApplyChangesLabel": "自動適用", - "common.ui.vis.editors.sidebar.autoApplyChangesTooltip": "変更を自動適用", - "common.ui.vis.editors.sidebar.discardChangesAriaLabel": "ビジュアライゼーションをリセット", - "common.ui.vis.editors.sidebar.discardChangesTooltip": "変更を破棄", - "common.ui.vis.editors.sidebar.errorButtonAriaLabel": "ハイライトされたフィールドのエラーを解決する必要があります。", "common.ui.vis.editors.sidebar.errorButtonTooltip": "ハイライトされたフィールドのエラーを解決する必要があります。", "common.ui.vis.editors.sidebar.tabs.dataLabel": "データ", "common.ui.vis.editors.sidebar.tabs.optionsLabel": "オプション", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", "common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。", - "common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", - "common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", - "common.ui.vis.visTypes.legend.loadingLabel": "読み込み中…", - "common.ui.vis.visTypes.legend.setColorScreenReaderDescription": "値 {legendDataLabel} の色を設定", - "common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", - "common.ui.vis.visTypes.legend.toggleLegendButtonTitle": "凡例を切り替える", - "common.ui.vis.visTypes.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", "common.ui.vislib.colormaps.bluesText": "青", "common.ui.vislib.colormaps.greensText": "緑", "common.ui.vislib.colormaps.greenToRedText": "緑から赤", @@ -1904,8 +1886,6 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", - "kbn.management.managementDescription": "Elastic Stack の管理を行うセンターコンソールです。", - "kbn.management.managementLabel": "管理", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除", "kbn.management.objects.confirmModalOptions.modalDescription": "削除されたオブジェクトは復元できません", "kbn.management.objects.confirmModalOptions.modalTitle": "保存された Kibana オブジェクトを削除しますか?", @@ -2491,183 +2471,192 @@ "kbn.visualize.wizard.step1Breadcrumb": "作成", "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", - "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", - "kbnVislibVisTypes.area.areaTitle": "エリア", - "kbnVislibVisTypes.area.countText": "カウント", - "kbnVislibVisTypes.area.groupTitle": "系列を分割", - "kbnVislibVisTypes.area.metricsTitle": "Y 軸", - "kbnVislibVisTypes.area.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.area.segmentTitle": "X 軸", - "kbnVislibVisTypes.area.splitTitle": "チャートを分割", - "kbnVislibVisTypes.area.tabs.metricsAxesTitle": "メトリックと軸", - "kbnVislibVisTypes.area.tabs.panelSettingsTitle": "パネル設定", - "kbnVislibVisTypes.axisModes.normalText": "標準", - "kbnVislibVisTypes.axisModes.percentageText": "パーセンテージ", - "kbnVislibVisTypes.axisModes.silhouetteText": "シルエット", - "kbnVislibVisTypes.axisModes.wiggleText": "振動", - "kbnVislibVisTypes.categoryAxis.rotate.angledText": "傾斜", - "kbnVislibVisTypes.categoryAxis.rotate.horizontalText": "横", - "kbnVislibVisTypes.categoryAxis.rotate.verticalText": "縦", - "kbnVislibVisTypes.chartModes.normalText": "標準", - "kbnVislibVisTypes.chartModes.stackedText": "スタック", - "kbnVislibVisTypes.chartTypes.areaText": "エリア", - "kbnVislibVisTypes.chartTypes.barText": "バー", - "kbnVislibVisTypes.chartTypes.lineText": "折れ線", - "kbnVislibVisTypes.controls.colorRanges.errorText": "各範囲は前の範囲よりも大きくなければなりません。", - "kbnVislibVisTypes.controls.colorSchema.colorSchemaLabel": "カラー図表", - "kbnVislibVisTypes.controls.colorSchema.howToChangeColorsDescription": "それぞれの色は凡例で変更できます。", - "kbnVislibVisTypes.controls.colorSchema.resetColorsButtonLabel": "色をリセット", - "kbnVislibVisTypes.controls.colorSchema.reverseColorSchemaLabel": "図表を反転", - "kbnVislibVisTypes.controls.gaugeOptions.alignmentLabel": "アラインメント", - "kbnVislibVisTypes.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", - "kbnVislibVisTypes.controls.gaugeOptions.displayWarningsLabel": "警告を表示", - "kbnVislibVisTypes.controls.gaugeOptions.extendRangeTooltip": "範囲をデータの最高値に広げます。", - "kbnVislibVisTypes.controls.gaugeOptions.gaugeTypeLabel": "ゲージタイプ", - "kbnVislibVisTypes.controls.gaugeOptions.labelsTitle": "ラベル", - "kbnVislibVisTypes.controls.gaugeOptions.percentageModeLabel": "パーセンテージモード", - "kbnVislibVisTypes.controls.gaugeOptions.rangesTitle": "範囲", - "kbnVislibVisTypes.controls.gaugeOptions.showLabelsLabel": "ラベルを表示", - "kbnVislibVisTypes.controls.gaugeOptions.showLegendLabel": "凡例を表示", - "kbnVislibVisTypes.controls.gaugeOptions.showScaleLabel": "縮尺を表示", - "kbnVislibVisTypes.controls.gaugeOptions.styleTitle": "スタイル", - "kbnVislibVisTypes.controls.gaugeOptions.subTextLabel": "サブラベル", - "kbnVislibVisTypes.controls.gaugeOptions.switchWarningsTooltip": "警告のオン・オフを切り替えます。オンにすると、すべてのラベルを表示できない際に警告が表示されます。", - "kbnVislibVisTypes.controls.heatmapOptions.colorLabel": "色", - "kbnVislibVisTypes.controls.heatmapOptions.colorScaleLabel": "カラースケール", - "kbnVislibVisTypes.controls.heatmapOptions.colorsNumberLabel": "色の数", - "kbnVislibVisTypes.controls.heatmapOptions.labelsTitle": "ラベル", - "kbnVislibVisTypes.controls.heatmapOptions.overwriteAutomaticColorLabel": "自動からーを上書きする", - "kbnVislibVisTypes.controls.heatmapOptions.percentageModeLabel": "パーセンテージモード", - "kbnVislibVisTypes.controls.heatmapOptions.rotateLabel": "回転", - "kbnVislibVisTypes.controls.heatmapOptions.scaleToDataBoundsLabel": "データバウンドに合わせる", - "kbnVislibVisTypes.controls.heatmapOptions.showLabelsTitle": "ラベルを表示", - "kbnVislibVisTypes.controls.heatmapOptions.useCustomRangesLabel": "カスタム範囲を使用", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.alignLabel": "配置", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.filterLabelsLabel": "フィルターラベル", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.labelsTitle": "ラベル", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.positionLabel": "配置", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabel": "表示", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabelsLabel": "ラベルを表示", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.xAxisTitle": "X 軸", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.dontShowLabel": "非表示", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.gridText": "グリッド", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.xAxisLinesLabel": "X 軸線を表示", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "ヒストグラムに X 軸線は表示できません。", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 軸線を表示", - "kbnVislibVisTypes.controls.pointSeries.series.chartTypeLabel": "チャートタイプ", - "kbnVislibVisTypes.controls.pointSeries.series.lineModeLabel": "線のモード", - "kbnVislibVisTypes.controls.pointSeries.series.lineWidthLabel": "線の幅", - "kbnVislibVisTypes.controls.pointSeries.series.metricsTitle": "メトリック", - "kbnVislibVisTypes.controls.pointSeries.series.modeLabel": "モード", - "kbnVislibVisTypes.controls.pointSeries.series.newAxisLabel": "新規軸…", - "kbnVislibVisTypes.controls.pointSeries.series.showDotsLabel": "点を表示", - "kbnVislibVisTypes.controls.pointSeries.series.showLineLabel": "線を表示", - "kbnVislibVisTypes.controls.pointSeries.series.valueAxisLabel": "値軸", - "kbnVislibVisTypes.controls.pointSeries.seriesAccordionAriaLabel": "{agg} オプションを切り替える", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.addButtonTooltip": "Y 軸を追加します", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.customExtentsLabel": "カスタム範囲", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.maxLabel": "最高", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minErrorMessage": "最低値は最高値よりも低く設定する必要があります", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minLabel": "最低", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minNeededScaleText": "ログスケールが選択されている場合、最低値は 0 よりも大きいものである必要があります", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.modeLabel": "モード", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.positionLabel": "配置", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.removeButtonTooltip": "Y 軸を削除します", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "境界マージン", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "境界マージンは 0 以上でなければなりません", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "データバウンドに合わせる", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleTypeLabel": "スケールタイプ", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.setAxisExtentsLabel": "軸の範囲の設定", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.showLabel": "表示", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.titleLabel": "タイトル", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "カスタム範囲を切り替える", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "{axisName} オプションを切り替える", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.yAxisTitle": "Y 軸", - "kbnVislibVisTypes.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", - "kbnVislibVisTypes.controls.truncateLabel": "切り捨て", - "kbnVislibVisTypes.controls.vislibBasicOptions.legendPositionLabel": "凡例の配置", - "kbnVislibVisTypes.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "kbnVislibVisTypes.editors.heatmap.basicSettingsTitle": "基本設定", - "kbnVislibVisTypes.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", - "kbnVislibVisTypes.editors.heatmap.highlightLabel": "ハイライト範囲", - "kbnVislibVisTypes.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr}).構成されている最高値は {max} です。", - "kbnVislibVisTypes.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。", - "kbnVislibVisTypes.editors.pie.donutLabel": "ドーナッツ", - "kbnVislibVisTypes.editors.pie.labelsSettingsTitle": "ラベル設定", - "kbnVislibVisTypes.editors.pie.pieSettingsTitle": "パイ設定", - "kbnVislibVisTypes.editors.pie.showLabelsLabel": "ラベルを表示", - "kbnVislibVisTypes.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", - "kbnVislibVisTypes.editors.pie.showValuesLabel": "値を表示", - "kbnVislibVisTypes.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", - "kbnVislibVisTypes.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", - "kbnVislibVisTypes.editors.pointSeries.settingsTitle": "設定", - "kbnVislibVisTypes.editors.pointSeries.showLabels": "チャートに値を表示", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.colorLabel": "ラインカラー", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.showLabel": "しきい線を表示", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.styleLabel": "ラインスタイル", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.valueLabel": "しきい値", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.widthLabel": "線の幅", - "kbnVislibVisTypes.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", - "kbnVislibVisTypes.functions.pie.help": "パイビジュアライゼーション", - "kbnVislibVisTypes.functions.vislib.help": "Vislib ビジュアライゼーション", - "kbnVislibVisTypes.gauge.alignmentAutomaticTitle": "自動", - "kbnVislibVisTypes.gauge.alignmentHorizontalTitle": "横", - "kbnVislibVisTypes.gauge.alignmentVerticalTitle": "縦", - "kbnVislibVisTypes.gauge.gaugeDescription": "ゲージはメトリックのステータスを示します。メトリックの値としきい値との関連性を示すのに使用します。", - "kbnVislibVisTypes.gauge.gaugeTitle": "ゲージ", - "kbnVislibVisTypes.gauge.gaugeTypes.arcText": "弧形", - "kbnVislibVisTypes.gauge.gaugeTypes.circleText": "円", - "kbnVislibVisTypes.gauge.groupTitle": "グループを分割", - "kbnVislibVisTypes.gauge.metricTitle": "メトリック", - "kbnVislibVisTypes.goal.goalDescription": "ゴールチャートは、最終目標にどれだけ近いかを示します。", - "kbnVislibVisTypes.goal.goalTitle": "ゴール", - "kbnVislibVisTypes.goal.groupTitle": "グループを分割", - "kbnVislibVisTypes.goal.metricTitle": "メトリック", - "kbnVislibVisTypes.heatmap.groupTitle": "Y 軸", - "kbnVislibVisTypes.heatmap.heatmapDescription": "マトリックス内のセルに影をつける。", - "kbnVislibVisTypes.heatmap.heatmapTitle": "ヒートマップ", - "kbnVislibVisTypes.heatmap.metricTitle": "値", - "kbnVislibVisTypes.heatmap.segmentTitle": "X 軸", - "kbnVislibVisTypes.heatmap.splitTitle": "チャートを分割", - "kbnVislibVisTypes.histogram.groupTitle": "系列を分割", - "kbnVislibVisTypes.histogram.histogramDescription": "連続変数を各軸に割り当てる。", - "kbnVislibVisTypes.histogram.histogramTitle": "縦棒", - "kbnVislibVisTypes.histogram.metricTitle": "Y 軸", - "kbnVislibVisTypes.histogram.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.histogram.segmentTitle": "X 軸", - "kbnVislibVisTypes.histogram.splitTitle": "チャートを分割", - "kbnVislibVisTypes.horizontalBar.groupTitle": "系列を分割", - "kbnVislibVisTypes.horizontalBar.horizontalBarDescription": "連続変数を各軸に割り当てる。", - "kbnVislibVisTypes.horizontalBar.horizontalBarTitle": "横棒", - "kbnVislibVisTypes.horizontalBar.metricTitle": "Y 軸", - "kbnVislibVisTypes.horizontalBar.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.horizontalBar.segmentTitle": "X 軸", - "kbnVislibVisTypes.horizontalBar.splitTitle": "チャートを分割", - "kbnVislibVisTypes.interpolationModes.smoothedText": "スムーズ", - "kbnVislibVisTypes.interpolationModes.steppedText": "ステップ", - "kbnVislibVisTypes.interpolationModes.straightText": "直線", - "kbnVislibVisTypes.legendPositions.bottomText": "一番下", - "kbnVislibVisTypes.legendPositions.leftText": "左", - "kbnVislibVisTypes.legendPositions.rightText": "右", - "kbnVislibVisTypes.legendPositions.topText": "一番上", - "kbnVislibVisTypes.line.groupTitle": "系列を分割", - "kbnVislibVisTypes.line.lineDescription": "トレンドを強調します。", - "kbnVislibVisTypes.line.lineTitle": "折れ線", - "kbnVislibVisTypes.line.metricTitle": "Y 軸", - "kbnVislibVisTypes.line.radiusTitle": "点のサイズ", - "kbnVislibVisTypes.line.segmentTitle": "X 軸", - "kbnVislibVisTypes.line.splitTitle": "チャートを分割", - "kbnVislibVisTypes.pie.metricTitle": "サイズのスライス", - "kbnVislibVisTypes.pie.pieDescription": "全体に対する内訳を表現する。", - "kbnVislibVisTypes.pie.pieTitle": "パイ", - "kbnVislibVisTypes.pie.segmentTitle": "スライスの分割", - "kbnVislibVisTypes.pie.splitTitle": "チャートを分割", - "kbnVislibVisTypes.scaleTypes.linearText": "直線", - "kbnVislibVisTypes.scaleTypes.logText": "ログ", - "kbnVislibVisTypes.scaleTypes.squareRootText": "平方根", - "kbnVislibVisTypes.thresholdLine.style.dashedText": "鎖線", - "kbnVislibVisTypes.thresholdLine.style.dotdashedText": "点線", - "kbnVislibVisTypes.thresholdLine.style.fullText": "完全", + "visTypeVislib.area.areaDescription": "折れ線グラフの下の数量を強調します。", + "visTypeVislib.area.areaTitle": "エリア", + "visTypeVislib.area.countText": "カウント", + "visTypeVislib.area.groupTitle": "系列を分割", + "visTypeVislib.area.metricsTitle": "Y 軸", + "visTypeVislib.area.radiusTitle": "点のサイズ", + "visTypeVislib.area.segmentTitle": "X 軸", + "visTypeVislib.area.splitTitle": "チャートを分割", + "visTypeVislib.area.tabs.metricsAxesTitle": "メトリックと軸", + "visTypeVislib.area.tabs.panelSettingsTitle": "パネル設定", + "visTypeVislib.axisModes.normalText": "標準", + "visTypeVislib.axisModes.percentageText": "パーセンテージ", + "visTypeVislib.axisModes.silhouetteText": "シルエット", + "visTypeVislib.axisModes.wiggleText": "振動", + "visTypeVislib.categoryAxis.rotate.angledText": "傾斜", + "visTypeVislib.categoryAxis.rotate.horizontalText": "横", + "visTypeVislib.categoryAxis.rotate.verticalText": "縦", + "visTypeVislib.chartModes.normalText": "標準", + "visTypeVislib.chartModes.stackedText": "スタック", + "visTypeVislib.chartTypes.areaText": "エリア", + "visTypeVislib.chartTypes.barText": "バー", + "visTypeVislib.chartTypes.lineText": "折れ線", + "visTypeVislib.controls.colorRanges.errorText": "各範囲は前の範囲よりも大きくなければなりません。", + "visTypeVislib.controls.colorSchema.colorSchemaLabel": "カラー図表", + "visTypeVislib.controls.colorSchema.howToChangeColorsDescription": "それぞれの色は凡例で変更できます。", + "visTypeVislib.controls.colorSchema.resetColorsButtonLabel": "色をリセット", + "visTypeVislib.controls.colorSchema.reverseColorSchemaLabel": "図表を反転", + "visTypeVislib.controls.gaugeOptions.alignmentLabel": "アラインメント", + "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "範囲を自動拡張", + "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "警告を表示", + "visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "範囲をデータの最高値に広げます。", + "visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "ゲージタイプ", + "visTypeVislib.controls.gaugeOptions.labelsTitle": "ラベル", + "visTypeVislib.controls.gaugeOptions.percentageModeLabel": "パーセンテージモード", + "visTypeVislib.controls.gaugeOptions.rangesTitle": "範囲", + "visTypeVislib.controls.gaugeOptions.showLabelsLabel": "ラベルを表示", + "visTypeVislib.controls.gaugeOptions.showLegendLabel": "凡例を表示", + "visTypeVislib.controls.gaugeOptions.showScaleLabel": "縮尺を表示", + "visTypeVislib.controls.gaugeOptions.styleTitle": "スタイル", + "visTypeVislib.controls.gaugeOptions.subTextLabel": "サブラベル", + "visTypeVislib.controls.gaugeOptions.switchWarningsTooltip": "警告のオン・オフを切り替えます。オンにすると、すべてのラベルを表示できない際に警告が表示されます。", + "visTypeVislib.controls.heatmapOptions.colorLabel": "色", + "visTypeVislib.controls.heatmapOptions.colorScaleLabel": "カラースケール", + "visTypeVislib.controls.heatmapOptions.colorsNumberLabel": "色の数", + "visTypeVislib.controls.heatmapOptions.labelsTitle": "ラベル", + "visTypeVislib.controls.heatmapOptions.overwriteAutomaticColorLabel": "自動からーを上書きする", + "visTypeVislib.controls.heatmapOptions.percentageModeLabel": "パーセンテージモード", + "visTypeVislib.controls.heatmapOptions.rotateLabel": "回転", + "visTypeVislib.controls.heatmapOptions.scaleToDataBoundsLabel": "データバウンドに合わせる", + "visTypeVislib.controls.heatmapOptions.showLabelsTitle": "ラベルを表示", + "visTypeVislib.controls.heatmapOptions.useCustomRangesLabel": "カスタム範囲を使用", + "visTypeVislib.controls.pointSeries.categoryAxis.alignLabel": "配置", + "visTypeVislib.controls.pointSeries.categoryAxis.filterLabelsLabel": "フィルターラベル", + "visTypeVislib.controls.pointSeries.categoryAxis.labelsTitle": "ラベル", + "visTypeVislib.controls.pointSeries.categoryAxis.positionLabel": "配置", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabel": "表示", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabelsLabel": "ラベルを表示", + "visTypeVislib.controls.pointSeries.categoryAxis.xAxisTitle": "X 軸", + "visTypeVislib.controls.pointSeries.gridAxis.dontShowLabel": "非表示", + "visTypeVislib.controls.pointSeries.gridAxis.gridText": "グリッド", + "visTypeVislib.controls.pointSeries.gridAxis.xAxisLinesLabel": "X 軸線を表示", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "ヒストグラムに X 軸線は表示できません。", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 軸線を表示", + "visTypeVislib.controls.pointSeries.series.chartTypeLabel": "チャートタイプ", + "visTypeVislib.controls.pointSeries.series.lineModeLabel": "線のモード", + "visTypeVislib.controls.pointSeries.series.lineWidthLabel": "線の幅", + "visTypeVislib.controls.pointSeries.series.metricsTitle": "メトリック", + "visTypeVislib.controls.pointSeries.series.modeLabel": "モード", + "visTypeVislib.controls.pointSeries.series.newAxisLabel": "新規軸…", + "visTypeVislib.controls.pointSeries.series.showDotsLabel": "点を表示", + "visTypeVislib.controls.pointSeries.series.showLineLabel": "線を表示", + "visTypeVislib.controls.pointSeries.series.valueAxisLabel": "値軸", + "visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel": "{agg} オプションを切り替える", + "visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip": "Y 軸を追加します", + "visTypeVislib.controls.pointSeries.valueAxes.customExtentsLabel": "カスタム範囲", + "visTypeVislib.controls.pointSeries.valueAxes.maxLabel": "最高", + "visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage": "最低値は最高値よりも低く設定する必要があります", + "visTypeVislib.controls.pointSeries.valueAxes.minLabel": "最低", + "visTypeVislib.controls.pointSeries.valueAxes.minNeededScaleText": "ログスケールが選択されている場合、最低値は 0 よりも大きいものである必要があります", + "visTypeVislib.controls.pointSeries.valueAxes.modeLabel": "モード", + "visTypeVislib.controls.pointSeries.valueAxes.positionLabel": "配置", + "visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip": "Y 軸を削除します", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "境界マージン", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "境界マージンは 0 以上でなければなりません", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "データバウンドに合わせる", + "visTypeVislib.controls.pointSeries.valueAxes.scaleTypeLabel": "スケールタイプ", + "visTypeVislib.controls.pointSeries.valueAxes.setAxisExtentsLabel": "軸の範囲の設定", + "visTypeVislib.controls.pointSeries.valueAxes.showLabel": "表示", + "visTypeVislib.controls.pointSeries.valueAxes.titleLabel": "タイトル", + "visTypeVislib.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "カスタム範囲を切り替える", + "visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "{axisName} オプションを切り替える", + "visTypeVislib.controls.pointSeries.valueAxes.yAxisTitle": "Y 軸", + "visTypeVislib.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", + "visTypeVislib.controls.truncateLabel": "切り捨て", + "visTypeVislib.controls.vislibBasicOptions.legendPositionLabel": "凡例の配置", + "visTypeVislib.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", + "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本設定", + "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定", + "visTypeVislib.editors.heatmap.highlightLabel": "ハイライト範囲", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr}).構成されている最高値は {max} です。", + "visTypeVislib.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。", + "visTypeVislib.editors.pie.donutLabel": "ドーナッツ", + "visTypeVislib.editors.pie.labelsSettingsTitle": "ラベル設定", + "visTypeVislib.editors.pie.pieSettingsTitle": "パイ設定", + "visTypeVislib.editors.pie.showLabelsLabel": "ラベルを表示", + "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", + "visTypeVislib.editors.pie.showValuesLabel": "値を表示", + "visTypeVislib.editors.pointSeries.currentTimeMarkerLabel": "現在時刻マーカー", + "visTypeVislib.editors.pointSeries.orderBucketsBySumLabel": "バケットを合計で並べ替え", + "visTypeVislib.editors.pointSeries.settingsTitle": "設定", + "visTypeVislib.editors.pointSeries.showLabels": "チャートに値を表示", + "visTypeVislib.editors.pointSeries.thresholdLine.colorLabel": "ラインカラー", + "visTypeVislib.editors.pointSeries.thresholdLine.showLabel": "しきい線を表示", + "visTypeVislib.editors.pointSeries.thresholdLine.styleLabel": "ラインスタイル", + "visTypeVislib.editors.pointSeries.thresholdLine.valueLabel": "しきい値", + "visTypeVislib.editors.pointSeries.thresholdLine.widthLabel": "線の幅", + "visTypeVislib.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", + "visTypeVislib.functions.pie.help": "パイビジュアライゼーション", + "visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション", + "visTypeVislib.gauge.alignmentAutomaticTitle": "自動", + "visTypeVislib.gauge.alignmentHorizontalTitle": "横", + "visTypeVislib.gauge.alignmentVerticalTitle": "縦", + "visTypeVislib.gauge.gaugeDescription": "ゲージはメトリックのステータスを示します。メトリックの値としきい値との関連性を示すのに使用します。", + "visTypeVislib.gauge.gaugeTitle": "ゲージ", + "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", + "visTypeVislib.gauge.gaugeTypes.circleText": "円", + "visTypeVislib.gauge.groupTitle": "グループを分割", + "visTypeVislib.gauge.metricTitle": "メトリック", + "visTypeVislib.goal.goalDescription": "ゴールチャートは、最終目標にどれだけ近いかを示します。", + "visTypeVislib.goal.goalTitle": "ゴール", + "visTypeVislib.goal.groupTitle": "グループを分割", + "visTypeVislib.goal.metricTitle": "メトリック", + "visTypeVislib.heatmap.groupTitle": "Y 軸", + "visTypeVislib.heatmap.heatmapDescription": "マトリックス内のセルに影をつける。", + "visTypeVislib.heatmap.heatmapTitle": "ヒートマップ", + "visTypeVislib.heatmap.metricTitle": "値", + "visTypeVislib.heatmap.segmentTitle": "X 軸", + "visTypeVislib.heatmap.splitTitle": "チャートを分割", + "visTypeVislib.histogram.groupTitle": "系列を分割", + "visTypeVislib.histogram.histogramDescription": "連続変数を各軸に割り当てる。", + "visTypeVislib.histogram.histogramTitle": "縦棒", + "visTypeVislib.histogram.metricTitle": "Y 軸", + "visTypeVislib.histogram.radiusTitle": "点のサイズ", + "visTypeVislib.histogram.segmentTitle": "X 軸", + "visTypeVislib.histogram.splitTitle": "チャートを分割", + "visTypeVislib.horizontalBar.groupTitle": "系列を分割", + "visTypeVislib.horizontalBar.horizontalBarDescription": "連続変数を各軸に割り当てる。", + "visTypeVislib.horizontalBar.horizontalBarTitle": "横棒", + "visTypeVislib.horizontalBar.metricTitle": "Y 軸", + "visTypeVislib.horizontalBar.radiusTitle": "点のサイズ", + "visTypeVislib.horizontalBar.segmentTitle": "X 軸", + "visTypeVislib.horizontalBar.splitTitle": "チャートを分割", + "visTypeVislib.interpolationModes.smoothedText": "スムーズ", + "visTypeVislib.interpolationModes.steppedText": "ステップ", + "visTypeVislib.interpolationModes.straightText": "直線", + "visTypeVislib.legendPositions.bottomText": "一番下", + "visTypeVislib.legendPositions.leftText": "左", + "visTypeVislib.legendPositions.rightText": "右", + "visTypeVislib.legendPositions.topText": "一番上", + "visTypeVislib.line.groupTitle": "系列を分割", + "visTypeVislib.line.lineDescription": "トレンドを強調します。", + "visTypeVislib.line.lineTitle": "折れ線", + "visTypeVislib.line.metricTitle": "Y 軸", + "visTypeVislib.line.radiusTitle": "点のサイズ", + "visTypeVislib.line.segmentTitle": "X 軸", + "visTypeVislib.line.splitTitle": "チャートを分割", + "visTypeVislib.pie.metricTitle": "サイズのスライス", + "visTypeVislib.pie.pieDescription": "全体に対する内訳を表現する。", + "visTypeVislib.pie.pieTitle": "パイ", + "visTypeVislib.pie.segmentTitle": "スライスの分割", + "visTypeVislib.pie.splitTitle": "チャートを分割", + "visTypeVislib.scaleTypes.linearText": "直線", + "visTypeVislib.scaleTypes.logText": "ログ", + "visTypeVislib.scaleTypes.squareRootText": "平方根", + "visTypeVislib.thresholdLine.style.dashedText": "鎖線", + "visTypeVislib.thresholdLine.style.dotdashedText": "点線", + "visTypeVislib.thresholdLine.style.fullText": "完全", + "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", + "visTypeVislib.vislib.tooltip.valueLabel": "値", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "値 {legendDataLabel} を除外", + "visTypeVislib.vislib.legend.loadingLabel": "読み込み中…", + "visTypeVislib.vislib.legend.setColorScreenReaderDescription": "値 {legendDataLabel} の色を設定", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "凡例を切り替える", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "全画面を終了", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを終了します。", "kibana-react.savedObjects.finder.filterButtonLabel": "タイプ", @@ -3149,12 +3138,12 @@ "visTypeMetric.function.bucket.help": "バケットディメンションの構成です。", "visTypeMetric.function.colorMode.help": "色を変更するメトリックの部分", "visTypeMetric.function.colorRange.help": "別の色が適用される値のグループを指定する範囲オブジェクト。", - "visTypeMetric.function.colorScheme.help": "使用する配色", + "visTypeMetric.function.colorSchema.help": "使用する配色", "visTypeMetric.function.font.help": "フォント設定です。", "visTypeMetric.function.help": "メトリックビジュアライゼーション", "visTypeMetric.function.invertColors.help": "色範囲を反転します", "visTypeMetric.function.metric.help": "メトリックディメンションの構成です。", - "visTypeMetric.function.percentage.help": "パーセンテージモードでメトリックを表示します。colorRange を設定する必要があります。", + "visTypeMetric.function.percentageMode.help": "パーセンテージモードでメトリックを表示します。colorRange を設定する必要があります。", "visTypeMetric.function.showLabels.help": "メトリック値の下にラベルを表示します。", "visTypeMetric.function.subText.help": "メトリックの下に表示するカスタムテキスト", "visTypeMetric.function.useRanges.help": "有効な色範囲です。", @@ -3221,7 +3210,6 @@ "visTypeTimeseries.addDeleteButtons.deleteButtonDefaultTooltip": "削除", "visTypeTimeseries.addDeleteButtons.reEnableTooltip": "再度有効にする", "visTypeTimeseries.addDeleteButtons.temporarilyDisableTooltip": "一時的に無効にする", - "visTypeTimeseries.aggLookup.addPipelineAggDescription": "{label} (「+」ボタンでこのパイプライン集約を追加します)", "visTypeTimeseries.aggLookup.averageLabel": "平均", "visTypeTimeseries.aggLookup.calculationLabel": "計算", "visTypeTimeseries.aggLookup.cardinalityLabel": "基数", @@ -5680,7 +5668,6 @@ "xpack.fileUpload.jsonIndexFilePicker.filePickerLabel": "アップロードするファイルを選択", "xpack.fileUpload.jsonIndexFilePicker.fileProcessingError": "ファイル処理エラー: {errorMessage}", "xpack.fileUpload.jsonIndexFilePicker.fileSizeError": "ファイルサイズエラー: {errorMessage}", - "xpack.fileUpload.jsonIndexFilePicker.formatsAccepted": "許可されている形式:.json、.geojson", "xpack.fileUpload.jsonIndexFilePicker.maxSize": "最大サイズ:{maxFileSize}", "xpack.fileUpload.jsonIndexFilePicker.noFileNameError": "ファイル名が指定されていません", "xpack.fileUpload.jsonIndexFilePicker.parsingFile": "{featuresProcessed} 件の機能が解析されました…", @@ -10604,17 +10591,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "ユーザー", "xpack.security.management.apiKeys.table.userNameColumnName": "ユーザー", "xpack.security.management.apiKeysTitle": "API キー", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "キャンセル", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "パスワードを変更", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "現在のパスワード", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "入力された現在のパスワードが正しくありません。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新しいパスワード", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.changePasswordForm.passwordLabel": "パスワード", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "変更を保存", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "Kibana ユーザーのパスワードを変更後、kibana.yml ファイルを更新し Kibana を再起動する必要があります。", "xpack.security.management.editRole.cancelButtonLabel": "キャンセル", "xpack.security.management.editRole.changeAllPrivilegesLink": "(すべて変更)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "非表示", @@ -10739,10 +10715,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "スペース権限を作成", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "グローバル特権を更新", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "スペース権限を更新", - "xpack.security.management.passwordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.passwordForm.passwordLabel": "パスワード", - "xpack.security.management.passwordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", "xpack.security.management.roles.actionsColumnName": "アクション", "xpack.security.management.roles.cloneRoleActionName": "{roleName} を複製", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "キャンセル", @@ -13243,4 +13215,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f3af5ec10338cd..2c2e5325969838 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -77,8 +77,6 @@ }, "messages": { "common.ui.aggResponse.allDocsTitle": "所有文档", - "common.ui.aggResponse.fieldLabel": "字段", - "common.ui.aggResponse.valueLabel": "值", "common.ui.aggTypes.aggNotValidLabel": "- 聚合无效 -", "common.ui.aggTypes.aggregateWith.noAggsErrorTooltip": "选择的字段没有兼容的聚合。", "common.ui.aggTypes.aggregateWithLabel": "聚合对象", @@ -452,7 +450,6 @@ "common.ui.flotCharts.thuLabel": "周四", "common.ui.flotCharts.tueLabel": "周二", "common.ui.flotCharts.wedLabel": "周三", - "common.ui.management.breadcrumb": "管理", "common.ui.modals.cancelButtonLabel": "取消", "common.ui.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", "common.ui.notify.fatalError.unavailableServerErrorMessage": "HTTP 请求无法连接。请检查 Kibana 服务器是否正在运行以及您的浏览器是否具有有效的连接,或请联系您的系统管理员。", @@ -503,27 +500,12 @@ "common.ui.vis.editors.aggGroups.bucketsText": "存储桶", "common.ui.vis.editors.aggGroups.metricsText": "指标", "common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage": "“{schema}” 聚合必须在所有其他存储桶之前运行!", - "common.ui.vis.editors.resizeAriaLabels": "按向左/向右键以调整编辑器的大小", - "common.ui.vis.editors.sidebar.applyChangesAriaLabel": "使用您的更改更新可视化", - "common.ui.vis.editors.sidebar.applyChangesTooltip": "应用更改", "common.ui.vis.editors.sidebar.autoApplyChangesAriaLabel": "每次更改时自动更新可视化", - "common.ui.vis.editors.sidebar.autoApplyChangesLabel": "自动应用", - "common.ui.vis.editors.sidebar.autoApplyChangesTooltip": "自动应用更改", - "common.ui.vis.editors.sidebar.discardChangesAriaLabel": "重置可视化", - "common.ui.vis.editors.sidebar.discardChangesTooltip": "放弃更改", - "common.ui.vis.editors.sidebar.errorButtonAriaLabel": "需要解决突出显示的字段中的错误。", "common.ui.vis.editors.sidebar.errorButtonTooltip": "需要解决突出显示的字段中的错误。", "common.ui.vis.editors.sidebar.tabs.dataLabel": "数据", "common.ui.vis.editors.sidebar.tabs.optionsLabel": "选项", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", "common.ui.vis.kibanaMap.zoomWarning": "已达到缩放级别最大数目。要一直放大,请升级到 Elasticsearch 和 Kibana 的 {defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", - "common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", - "common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", - "common.ui.vis.visTypes.legend.loadingLabel": "正在加载……", - "common.ui.vis.visTypes.legend.setColorScreenReaderDescription": "为值 {legendDataLabel} 设置颜色", - "common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel": "切换图例", - "common.ui.vis.visTypes.legend.toggleLegendButtonTitle": "切换图例", - "common.ui.vis.visTypes.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}切换选项", "common.ui.vislib.colormaps.bluesText": "蓝色", "common.ui.vislib.colormaps.greensText": "绿色", "common.ui.vislib.colormaps.greenToRedText": "绿到红", @@ -1904,8 +1886,6 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "在左侧菜单中可找到完整工具列表", - "kbn.management.managementDescription": "您用于管理 Elastic Stack 的中心控制台。", - "kbn.management.managementLabel": "管理", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除", "kbn.management.objects.confirmModalOptions.modalDescription": "您无法恢复删除的对象", "kbn.management.objects.confirmModalOptions.modalTitle": "删除已保存 Kibana 对象?", @@ -2491,183 +2471,192 @@ "kbn.visualize.wizard.step1Breadcrumb": "创建", "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", - "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", - "kbnVislibVisTypes.area.areaTitle": "面积图", - "kbnVislibVisTypes.area.countText": "计数", - "kbnVislibVisTypes.area.groupTitle": "拆分序列", - "kbnVislibVisTypes.area.metricsTitle": "Y 轴", - "kbnVislibVisTypes.area.radiusTitle": "点大小", - "kbnVislibVisTypes.area.segmentTitle": "X 轴", - "kbnVislibVisTypes.area.splitTitle": "拆分图表", - "kbnVislibVisTypes.area.tabs.metricsAxesTitle": "指标和轴", - "kbnVislibVisTypes.area.tabs.panelSettingsTitle": "面板设置", - "kbnVislibVisTypes.axisModes.normalText": "正常", - "kbnVislibVisTypes.axisModes.percentageText": "百分比", - "kbnVislibVisTypes.axisModes.silhouetteText": "剪影", - "kbnVislibVisTypes.axisModes.wiggleText": "扭动", - "kbnVislibVisTypes.categoryAxis.rotate.angledText": "带角度", - "kbnVislibVisTypes.categoryAxis.rotate.horizontalText": "水平", - "kbnVislibVisTypes.categoryAxis.rotate.verticalText": "垂直", - "kbnVislibVisTypes.chartModes.normalText": "正常", - "kbnVislibVisTypes.chartModes.stackedText": "堆叠", - "kbnVislibVisTypes.chartTypes.areaText": "面积图", - "kbnVislibVisTypes.chartTypes.barText": "条形图", - "kbnVislibVisTypes.chartTypes.lineText": "折线图", - "kbnVislibVisTypes.controls.colorRanges.errorText": "每个范围应大于前一范围。", - "kbnVislibVisTypes.controls.colorSchema.colorSchemaLabel": "颜色模式", - "kbnVislibVisTypes.controls.colorSchema.howToChangeColorsDescription": "可以更改图例中的各个颜色。", - "kbnVislibVisTypes.controls.colorSchema.resetColorsButtonLabel": "重置颜色", - "kbnVislibVisTypes.controls.colorSchema.reverseColorSchemaLabel": "反转模式", - "kbnVislibVisTypes.controls.gaugeOptions.alignmentLabel": "对齐方式", - "kbnVislibVisTypes.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", - "kbnVislibVisTypes.controls.gaugeOptions.displayWarningsLabel": "显示警告", - "kbnVislibVisTypes.controls.gaugeOptions.extendRangeTooltip": "将数据范围扩展到最大值。", - "kbnVislibVisTypes.controls.gaugeOptions.gaugeTypeLabel": "仪表类型", - "kbnVislibVisTypes.controls.gaugeOptions.labelsTitle": "标签", - "kbnVislibVisTypes.controls.gaugeOptions.percentageModeLabel": "百分比模式", - "kbnVislibVisTypes.controls.gaugeOptions.rangesTitle": "范围", - "kbnVislibVisTypes.controls.gaugeOptions.showLabelsLabel": "显示标签", - "kbnVislibVisTypes.controls.gaugeOptions.showLegendLabel": "显示图例", - "kbnVislibVisTypes.controls.gaugeOptions.showScaleLabel": "显示比例", - "kbnVislibVisTypes.controls.gaugeOptions.styleTitle": "样式", - "kbnVislibVisTypes.controls.gaugeOptions.subTextLabel": "子标签", - "kbnVislibVisTypes.controls.gaugeOptions.switchWarningsTooltip": "打开/关闭警告。打开时,如果标签没有全部显示,则显示警告。", - "kbnVislibVisTypes.controls.heatmapOptions.colorLabel": "颜色", - "kbnVislibVisTypes.controls.heatmapOptions.colorScaleLabel": "色阶", - "kbnVislibVisTypes.controls.heatmapOptions.colorsNumberLabel": "颜色个数", - "kbnVislibVisTypes.controls.heatmapOptions.labelsTitle": "标签", - "kbnVislibVisTypes.controls.heatmapOptions.overwriteAutomaticColorLabel": "覆盖自动配色", - "kbnVislibVisTypes.controls.heatmapOptions.percentageModeLabel": "百分比模式", - "kbnVislibVisTypes.controls.heatmapOptions.rotateLabel": "旋转", - "kbnVislibVisTypes.controls.heatmapOptions.scaleToDataBoundsLabel": "缩放到数据边界", - "kbnVislibVisTypes.controls.heatmapOptions.showLabelsTitle": "显示标签", - "kbnVislibVisTypes.controls.heatmapOptions.useCustomRangesLabel": "使用定制范围", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.alignLabel": "对齐", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.filterLabelsLabel": "筛选标签", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.labelsTitle": "标签", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.positionLabel": "位置", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabel": "显示", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.showLabelsLabel": "显示标签", - "kbnVislibVisTypes.controls.pointSeries.categoryAxis.xAxisTitle": "X 轴", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.dontShowLabel": "不显示", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.gridText": "网格", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.xAxisLinesLabel": "显示 X 轴线", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "直方图的 X 轴线无法显示。", - "kbnVislibVisTypes.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 轴线", - "kbnVislibVisTypes.controls.pointSeries.series.chartTypeLabel": "图表类型", - "kbnVislibVisTypes.controls.pointSeries.series.lineModeLabel": "线条模式", - "kbnVislibVisTypes.controls.pointSeries.series.lineWidthLabel": "线条宽度", - "kbnVislibVisTypes.controls.pointSeries.series.metricsTitle": "指标", - "kbnVislibVisTypes.controls.pointSeries.series.modeLabel": "模式", - "kbnVislibVisTypes.controls.pointSeries.series.newAxisLabel": "新建轴…...", - "kbnVislibVisTypes.controls.pointSeries.series.showDotsLabel": "显示点线", - "kbnVislibVisTypes.controls.pointSeries.series.showLineLabel": "显示为线条", - "kbnVislibVisTypes.controls.pointSeries.series.valueAxisLabel": "值轴", - "kbnVislibVisTypes.controls.pointSeries.seriesAccordionAriaLabel": "切换 {agg} 选项", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.addButtonTooltip": "添加 Y 轴", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.customExtentsLabel": "定制范围", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.maxLabel": "最大值", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minErrorMessage": "最小值应小于最大值。", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minLabel": "最小值", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.minNeededScaleText": "如果选择了对数刻度,最小值必须大于 0。", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.modeLabel": "模式", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.positionLabel": "位置", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.removeButtonTooltip": "移除 Y 轴", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "边界边距", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "边界边距必须大于或等于 0。", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "缩放到数据边界", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.scaleTypeLabel": "缩放类型", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.setAxisExtentsLabel": "设置轴范围", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.showLabel": "显示", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.titleLabel": "标题", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "切换定制范围", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "切换 {axisName} 选项", - "kbnVislibVisTypes.controls.pointSeries.valueAxes.yAxisTitle": "Y 轴", - "kbnVislibVisTypes.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", - "kbnVislibVisTypes.controls.truncateLabel": "截断", - "kbnVislibVisTypes.controls.vislibBasicOptions.legendPositionLabel": "图例位置", - "kbnVislibVisTypes.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "kbnVislibVisTypes.editors.heatmap.basicSettingsTitle": "基本设置", - "kbnVislibVisTypes.editors.heatmap.heatmapSettingsTitle": "热图设置", - "kbnVislibVisTypes.editors.heatmap.highlightLabel": "高亮范围", - "kbnVislibVisTypes.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", - "kbnVislibVisTypes.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。", - "kbnVislibVisTypes.editors.pie.donutLabel": "圆环图", - "kbnVislibVisTypes.editors.pie.labelsSettingsTitle": "标签设置", - "kbnVislibVisTypes.editors.pie.pieSettingsTitle": "饼图设置", - "kbnVislibVisTypes.editors.pie.showLabelsLabel": "显示标签", - "kbnVislibVisTypes.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", - "kbnVislibVisTypes.editors.pie.showValuesLabel": "显示值", - "kbnVislibVisTypes.editors.pointSeries.currentTimeMarkerLabel": "当前时间标记", - "kbnVislibVisTypes.editors.pointSeries.orderBucketsBySumLabel": "按总计值排序存储桶", - "kbnVislibVisTypes.editors.pointSeries.settingsTitle": "设置", - "kbnVislibVisTypes.editors.pointSeries.showLabels": "在图表上显示值", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.colorLabel": "线条颜色", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.showLabel": "显示阈值线条", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.styleLabel": "线条样式", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.valueLabel": "阈值", - "kbnVislibVisTypes.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", - "kbnVislibVisTypes.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", - "kbnVislibVisTypes.functions.pie.help": "饼图可视化", - "kbnVislibVisTypes.functions.vislib.help": "Vislib 可视化", - "kbnVislibVisTypes.gauge.alignmentAutomaticTitle": "自动", - "kbnVislibVisTypes.gauge.alignmentHorizontalTitle": "水平", - "kbnVislibVisTypes.gauge.alignmentVerticalTitle": "垂直", - "kbnVislibVisTypes.gauge.gaugeDescription": "仪表盘图指示指标的状态。用于显示指标值与参考阈值的相关程度。", - "kbnVislibVisTypes.gauge.gaugeTitle": "仪表盘图", - "kbnVislibVisTypes.gauge.gaugeTypes.arcText": "弧形", - "kbnVislibVisTypes.gauge.gaugeTypes.circleText": "圆形", - "kbnVislibVisTypes.gauge.groupTitle": "拆分组", - "kbnVislibVisTypes.gauge.metricTitle": "指标", - "kbnVislibVisTypes.goal.goalDescription": "目标图指示与最终目标的接近程度。", - "kbnVislibVisTypes.goal.goalTitle": "目标图", - "kbnVislibVisTypes.goal.groupTitle": "拆分组", - "kbnVislibVisTypes.goal.metricTitle": "指标", - "kbnVislibVisTypes.heatmap.groupTitle": "Y 轴", - "kbnVislibVisTypes.heatmap.heatmapDescription": "为矩阵中的单元格添加阴影", - "kbnVislibVisTypes.heatmap.heatmapTitle": "热力图", - "kbnVislibVisTypes.heatmap.metricTitle": "值", - "kbnVislibVisTypes.heatmap.segmentTitle": "X 轴", - "kbnVislibVisTypes.heatmap.splitTitle": "拆分图表", - "kbnVislibVisTypes.histogram.groupTitle": "拆分序列", - "kbnVislibVisTypes.histogram.histogramDescription": "向每个轴赋予连续变量", - "kbnVislibVisTypes.histogram.histogramTitle": "垂直条形图", - "kbnVislibVisTypes.histogram.metricTitle": "Y 轴", - "kbnVislibVisTypes.histogram.radiusTitle": "点大小", - "kbnVislibVisTypes.histogram.segmentTitle": "X 轴", - "kbnVislibVisTypes.histogram.splitTitle": "拆分图表", - "kbnVislibVisTypes.horizontalBar.groupTitle": "拆分序列", - "kbnVislibVisTypes.horizontalBar.horizontalBarDescription": "向每个轴赋予连续变量", - "kbnVislibVisTypes.horizontalBar.horizontalBarTitle": "水平条形图", - "kbnVislibVisTypes.horizontalBar.metricTitle": "Y 轴", - "kbnVislibVisTypes.horizontalBar.radiusTitle": "点大小", - "kbnVislibVisTypes.horizontalBar.segmentTitle": "X 轴", - "kbnVislibVisTypes.horizontalBar.splitTitle": "拆分图表", - "kbnVislibVisTypes.interpolationModes.smoothedText": "平滑", - "kbnVislibVisTypes.interpolationModes.steppedText": "渐变", - "kbnVislibVisTypes.interpolationModes.straightText": "直线", - "kbnVislibVisTypes.legendPositions.bottomText": "下", - "kbnVislibVisTypes.legendPositions.leftText": "左", - "kbnVislibVisTypes.legendPositions.rightText": "右", - "kbnVislibVisTypes.legendPositions.topText": "上", - "kbnVislibVisTypes.line.groupTitle": "拆分序列", - "kbnVislibVisTypes.line.lineDescription": "突出趋势", - "kbnVislibVisTypes.line.lineTitle": "折线图", - "kbnVislibVisTypes.line.metricTitle": "Y 轴", - "kbnVislibVisTypes.line.radiusTitle": "点大小", - "kbnVislibVisTypes.line.segmentTitle": "X 轴", - "kbnVislibVisTypes.line.splitTitle": "拆分图表", - "kbnVislibVisTypes.pie.metricTitle": "切片大小", - "kbnVislibVisTypes.pie.pieDescription": "比较整体的各个部分", - "kbnVislibVisTypes.pie.pieTitle": "饼图", - "kbnVislibVisTypes.pie.segmentTitle": "拆分切片", - "kbnVislibVisTypes.pie.splitTitle": "拆分图表", - "kbnVislibVisTypes.scaleTypes.linearText": "线性", - "kbnVislibVisTypes.scaleTypes.logText": "对数", - "kbnVislibVisTypes.scaleTypes.squareRootText": "平方根", - "kbnVislibVisTypes.thresholdLine.style.dashedText": "虚线", - "kbnVislibVisTypes.thresholdLine.style.dotdashedText": "点虚线", - "kbnVislibVisTypes.thresholdLine.style.fullText": "实线", + "visTypeVislib.area.areaDescription": "突出折线图下方的数量", + "visTypeVislib.area.areaTitle": "面积图", + "visTypeVislib.area.countText": "计数", + "visTypeVislib.area.groupTitle": "拆分序列", + "visTypeVislib.area.metricsTitle": "Y 轴", + "visTypeVislib.area.radiusTitle": "点大小", + "visTypeVislib.area.segmentTitle": "X 轴", + "visTypeVislib.area.splitTitle": "拆分图表", + "visTypeVislib.area.tabs.metricsAxesTitle": "指标和轴", + "visTypeVislib.area.tabs.panelSettingsTitle": "面板设置", + "visTypeVislib.axisModes.normalText": "正常", + "visTypeVislib.axisModes.percentageText": "百分比", + "visTypeVislib.axisModes.silhouetteText": "剪影", + "visTypeVislib.axisModes.wiggleText": "扭动", + "visTypeVislib.categoryAxis.rotate.angledText": "带角度", + "visTypeVislib.categoryAxis.rotate.horizontalText": "水平", + "visTypeVislib.categoryAxis.rotate.verticalText": "垂直", + "visTypeVislib.chartModes.normalText": "正常", + "visTypeVislib.chartModes.stackedText": "堆叠", + "visTypeVislib.chartTypes.areaText": "面积图", + "visTypeVislib.chartTypes.barText": "条形图", + "visTypeVislib.chartTypes.lineText": "折线图", + "visTypeVislib.controls.colorRanges.errorText": "每个范围应大于前一范围。", + "visTypeVislib.controls.colorSchema.colorSchemaLabel": "颜色模式", + "visTypeVislib.controls.colorSchema.howToChangeColorsDescription": "可以更改图例中的各个颜色。", + "visTypeVislib.controls.colorSchema.resetColorsButtonLabel": "重置颜色", + "visTypeVislib.controls.colorSchema.reverseColorSchemaLabel": "反转模式", + "visTypeVislib.controls.gaugeOptions.alignmentLabel": "对齐方式", + "visTypeVislib.controls.gaugeOptions.autoExtendRangeLabel": "自动扩展范围", + "visTypeVislib.controls.gaugeOptions.displayWarningsLabel": "显示警告", + "visTypeVislib.controls.gaugeOptions.extendRangeTooltip": "将数据范围扩展到最大值。", + "visTypeVislib.controls.gaugeOptions.gaugeTypeLabel": "仪表类型", + "visTypeVislib.controls.gaugeOptions.labelsTitle": "标签", + "visTypeVislib.controls.gaugeOptions.percentageModeLabel": "百分比模式", + "visTypeVislib.controls.gaugeOptions.rangesTitle": "范围", + "visTypeVislib.controls.gaugeOptions.showLabelsLabel": "显示标签", + "visTypeVislib.controls.gaugeOptions.showLegendLabel": "显示图例", + "visTypeVislib.controls.gaugeOptions.showScaleLabel": "显示比例", + "visTypeVislib.controls.gaugeOptions.styleTitle": "样式", + "visTypeVislib.controls.gaugeOptions.subTextLabel": "子标签", + "visTypeVislib.controls.gaugeOptions.switchWarningsTooltip": "打开/关闭警告。打开时,如果标签没有全部显示,则显示警告。", + "visTypeVislib.controls.heatmapOptions.colorLabel": "颜色", + "visTypeVislib.controls.heatmapOptions.colorScaleLabel": "色阶", + "visTypeVislib.controls.heatmapOptions.colorsNumberLabel": "颜色个数", + "visTypeVislib.controls.heatmapOptions.labelsTitle": "标签", + "visTypeVislib.controls.heatmapOptions.overwriteAutomaticColorLabel": "覆盖自动配色", + "visTypeVislib.controls.heatmapOptions.percentageModeLabel": "百分比模式", + "visTypeVislib.controls.heatmapOptions.rotateLabel": "旋转", + "visTypeVislib.controls.heatmapOptions.scaleToDataBoundsLabel": "缩放到数据边界", + "visTypeVislib.controls.heatmapOptions.showLabelsTitle": "显示标签", + "visTypeVislib.controls.heatmapOptions.useCustomRangesLabel": "使用定制范围", + "visTypeVislib.controls.pointSeries.categoryAxis.alignLabel": "对齐", + "visTypeVislib.controls.pointSeries.categoryAxis.filterLabelsLabel": "筛选标签", + "visTypeVislib.controls.pointSeries.categoryAxis.labelsTitle": "标签", + "visTypeVislib.controls.pointSeries.categoryAxis.positionLabel": "位置", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabel": "显示", + "visTypeVislib.controls.pointSeries.categoryAxis.showLabelsLabel": "显示标签", + "visTypeVislib.controls.pointSeries.categoryAxis.xAxisTitle": "X 轴", + "visTypeVislib.controls.pointSeries.gridAxis.dontShowLabel": "不显示", + "visTypeVislib.controls.pointSeries.gridAxis.gridText": "网格", + "visTypeVislib.controls.pointSeries.gridAxis.xAxisLinesLabel": "显示 X 轴线", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "直方图的 X 轴线无法显示。", + "visTypeVislib.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 轴线", + "visTypeVislib.controls.pointSeries.series.chartTypeLabel": "图表类型", + "visTypeVislib.controls.pointSeries.series.lineModeLabel": "线条模式", + "visTypeVislib.controls.pointSeries.series.lineWidthLabel": "线条宽度", + "visTypeVislib.controls.pointSeries.series.metricsTitle": "指标", + "visTypeVislib.controls.pointSeries.series.modeLabel": "模式", + "visTypeVislib.controls.pointSeries.series.newAxisLabel": "新建轴…...", + "visTypeVislib.controls.pointSeries.series.showDotsLabel": "显示点线", + "visTypeVislib.controls.pointSeries.series.showLineLabel": "显示为线条", + "visTypeVislib.controls.pointSeries.series.valueAxisLabel": "值轴", + "visTypeVislib.controls.pointSeries.seriesAccordionAriaLabel": "切换 {agg} 选项", + "visTypeVislib.controls.pointSeries.valueAxes.addButtonTooltip": "添加 Y 轴", + "visTypeVislib.controls.pointSeries.valueAxes.customExtentsLabel": "定制范围", + "visTypeVislib.controls.pointSeries.valueAxes.maxLabel": "最大值", + "visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage": "最小值应小于最大值。", + "visTypeVislib.controls.pointSeries.valueAxes.minLabel": "最小值", + "visTypeVislib.controls.pointSeries.valueAxes.minNeededScaleText": "如果选择了对数刻度,最小值必须大于 0。", + "visTypeVislib.controls.pointSeries.valueAxes.modeLabel": "模式", + "visTypeVislib.controls.pointSeries.valueAxes.positionLabel": "位置", + "visTypeVislib.controls.pointSeries.valueAxes.removeButtonTooltip": "移除 Y 轴", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.boundsMargin": "边界边距", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBounds.minNeededBoundsMargin": "边界边距必须大于或等于 0。", + "visTypeVislib.controls.pointSeries.valueAxes.scaleToDataBoundsLabel": "缩放到数据边界", + "visTypeVislib.controls.pointSeries.valueAxes.scaleTypeLabel": "缩放类型", + "visTypeVislib.controls.pointSeries.valueAxes.setAxisExtentsLabel": "设置轴范围", + "visTypeVislib.controls.pointSeries.valueAxes.showLabel": "显示", + "visTypeVislib.controls.pointSeries.valueAxes.titleLabel": "标题", + "visTypeVislib.controls.pointSeries.valueAxes.toggleCustomExtendsAriaLabel": "切换定制范围", + "visTypeVislib.controls.pointSeries.valueAxes.toggleOptionsAriaLabel": "切换 {axisName} 选项", + "visTypeVislib.controls.pointSeries.valueAxes.yAxisTitle": "Y 轴", + "visTypeVislib.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", + "visTypeVislib.controls.truncateLabel": "截断", + "visTypeVislib.controls.vislibBasicOptions.legendPositionLabel": "图例位置", + "visTypeVislib.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", + "visTypeVislib.editors.heatmap.basicSettingsTitle": "基本设置", + "visTypeVislib.editors.heatmap.heatmapSettingsTitle": "热图设置", + "visTypeVislib.editors.heatmap.highlightLabel": "高亮范围", + "visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。", + "visTypeVislib.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。", + "visTypeVislib.editors.pie.donutLabel": "圆环图", + "visTypeVislib.editors.pie.labelsSettingsTitle": "标签设置", + "visTypeVislib.editors.pie.pieSettingsTitle": "饼图设置", + "visTypeVislib.editors.pie.showLabelsLabel": "显示标签", + "visTypeVislib.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", + "visTypeVislib.editors.pie.showValuesLabel": "显示值", + "visTypeVislib.editors.pointSeries.currentTimeMarkerLabel": "当前时间标记", + "visTypeVislib.editors.pointSeries.orderBucketsBySumLabel": "按总计值排序存储桶", + "visTypeVislib.editors.pointSeries.settingsTitle": "设置", + "visTypeVislib.editors.pointSeries.showLabels": "在图表上显示值", + "visTypeVislib.editors.pointSeries.thresholdLine.colorLabel": "线条颜色", + "visTypeVislib.editors.pointSeries.thresholdLine.showLabel": "显示阈值线条", + "visTypeVislib.editors.pointSeries.thresholdLine.styleLabel": "线条样式", + "visTypeVislib.editors.pointSeries.thresholdLine.valueLabel": "阈值", + "visTypeVislib.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", + "visTypeVislib.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", + "visTypeVislib.functions.pie.help": "饼图可视化", + "visTypeVislib.functions.vislib.help": "Vislib 可视化", + "visTypeVislib.gauge.alignmentAutomaticTitle": "自动", + "visTypeVislib.gauge.alignmentHorizontalTitle": "水平", + "visTypeVislib.gauge.alignmentVerticalTitle": "垂直", + "visTypeVislib.gauge.gaugeDescription": "仪表盘图指示指标的状态。用于显示指标值与参考阈值的相关程度。", + "visTypeVislib.gauge.gaugeTitle": "仪表盘图", + "visTypeVislib.gauge.gaugeTypes.arcText": "弧形", + "visTypeVislib.gauge.gaugeTypes.circleText": "圆形", + "visTypeVislib.gauge.groupTitle": "拆分组", + "visTypeVislib.gauge.metricTitle": "指标", + "visTypeVislib.goal.goalDescription": "目标图指示与最终目标的接近程度。", + "visTypeVislib.goal.goalTitle": "目标图", + "visTypeVislib.goal.groupTitle": "拆分组", + "visTypeVislib.goal.metricTitle": "指标", + "visTypeVislib.heatmap.groupTitle": "Y 轴", + "visTypeVislib.heatmap.heatmapDescription": "为矩阵中的单元格添加阴影", + "visTypeVislib.heatmap.heatmapTitle": "热力图", + "visTypeVislib.heatmap.metricTitle": "值", + "visTypeVislib.heatmap.segmentTitle": "X 轴", + "visTypeVislib.heatmap.splitTitle": "拆分图表", + "visTypeVislib.histogram.groupTitle": "拆分序列", + "visTypeVislib.histogram.histogramDescription": "向每个轴赋予连续变量", + "visTypeVislib.histogram.histogramTitle": "垂直条形图", + "visTypeVislib.histogram.metricTitle": "Y 轴", + "visTypeVislib.histogram.radiusTitle": "点大小", + "visTypeVislib.histogram.segmentTitle": "X 轴", + "visTypeVislib.histogram.splitTitle": "拆分图表", + "visTypeVislib.horizontalBar.groupTitle": "拆分序列", + "visTypeVislib.horizontalBar.horizontalBarDescription": "向每个轴赋予连续变量", + "visTypeVislib.horizontalBar.horizontalBarTitle": "水平条形图", + "visTypeVislib.horizontalBar.metricTitle": "Y 轴", + "visTypeVislib.horizontalBar.radiusTitle": "点大小", + "visTypeVislib.horizontalBar.segmentTitle": "X 轴", + "visTypeVislib.horizontalBar.splitTitle": "拆分图表", + "visTypeVislib.interpolationModes.smoothedText": "平滑", + "visTypeVislib.interpolationModes.steppedText": "渐变", + "visTypeVislib.interpolationModes.straightText": "直线", + "visTypeVislib.legendPositions.bottomText": "下", + "visTypeVislib.legendPositions.leftText": "左", + "visTypeVislib.legendPositions.rightText": "右", + "visTypeVislib.legendPositions.topText": "上", + "visTypeVislib.line.groupTitle": "拆分序列", + "visTypeVislib.line.lineDescription": "突出趋势", + "visTypeVislib.line.lineTitle": "折线图", + "visTypeVislib.line.metricTitle": "Y 轴", + "visTypeVislib.line.radiusTitle": "点大小", + "visTypeVislib.line.segmentTitle": "X 轴", + "visTypeVislib.line.splitTitle": "拆分图表", + "visTypeVislib.pie.metricTitle": "切片大小", + "visTypeVislib.pie.pieDescription": "比较整体的各个部分", + "visTypeVislib.pie.pieTitle": "饼图", + "visTypeVislib.pie.segmentTitle": "拆分切片", + "visTypeVislib.pie.splitTitle": "拆分图表", + "visTypeVislib.scaleTypes.linearText": "线性", + "visTypeVislib.scaleTypes.logText": "对数", + "visTypeVislib.scaleTypes.squareRootText": "平方根", + "visTypeVislib.thresholdLine.style.dashedText": "虚线", + "visTypeVislib.thresholdLine.style.dotdashedText": "点虚线", + "visTypeVislib.thresholdLine.style.fullText": "实线", + "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", + "visTypeVislib.vislib.tooltip.valueLabel": "値", + "visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}", + "visTypeVislib.vislib.legend.filterOutValueButtonAriaLabel": "筛除值 {legendDataLabel}", + "visTypeVislib.vislib.legend.loadingLabel": "正在加载……", + "visTypeVislib.vislib.legend.setColorScreenReaderDescription": "为值 {legendDataLabel} 设置颜色", + "visTypeVislib.vislib.legend.toggleLegendButtonAriaLabel": "切换图例", + "visTypeVislib.vislib.legend.toggleLegendButtonTitle": "切换图例", + "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}切换选项", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "退出全屏", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。", "kibana-react.savedObjects.finder.filterButtonLabel": "类型", @@ -3149,12 +3138,12 @@ "visTypeMetric.function.bucket.help": "存储桶维度配置", "visTypeMetric.function.colorMode.help": "指标的哪部分要上色", "visTypeMetric.function.colorRange.help": "指定应将不同颜色应用到的值组的范围对象。", - "visTypeMetric.function.colorScheme.help": "要使用的颜色方案", + "visTypeMetric.function.colorSchema.help": "要使用的颜色方案", "visTypeMetric.function.font.help": "字体设置。", "visTypeMetric.function.help": "指标可视化", "visTypeMetric.function.invertColors.help": "反转颜色范围", "visTypeMetric.function.metric.help": "指标维度配置", - "visTypeMetric.function.percentage.help": "以百分比模式显示指标。需要设置 colorRange。", + "visTypeMetric.function.percentageMode.help": "以百分比模式显示指标。需要设置 colorRange。", "visTypeMetric.function.showLabels.help": "在指标值下显示标签。", "visTypeMetric.function.subText.help": "要在指标下显示的定制文本", "visTypeMetric.function.useRanges.help": "已启用颜色范围。", @@ -3221,7 +3210,6 @@ "visTypeTimeseries.addDeleteButtons.deleteButtonDefaultTooltip": "删除", "visTypeTimeseries.addDeleteButtons.reEnableTooltip": "重新启用", "visTypeTimeseries.addDeleteButtons.temporarilyDisableTooltip": "暂时禁用", - "visTypeTimeseries.aggLookup.addPipelineAggDescription": "{label}(使用“+”按钮添加此管道聚合)", "visTypeTimeseries.aggLookup.averageLabel": "平均值", "visTypeTimeseries.aggLookup.calculationLabel": "计算", "visTypeTimeseries.aggLookup.cardinalityLabel": "基数", @@ -5679,7 +5667,6 @@ "xpack.fileUpload.jsonIndexFilePicker.filePickerLabel": "选择文件进行上传", "xpack.fileUpload.jsonIndexFilePicker.fileProcessingError": "文件处理错误:{errorMessage}", "xpack.fileUpload.jsonIndexFilePicker.fileSizeError": "文件大小错误:{errorMessage}", - "xpack.fileUpload.jsonIndexFilePicker.formatsAccepted": "接受的格式:.json、.geojson", "xpack.fileUpload.jsonIndexFilePicker.maxSize": "最大大小:{maxFileSize}", "xpack.fileUpload.jsonIndexFilePicker.noFileNameError": "未提供任何文件名称", "xpack.fileUpload.jsonIndexFilePicker.parsingFile": "{featuresProcessed} 特征已解析......", @@ -10603,17 +10590,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "用户", "xpack.security.management.apiKeys.table.userNameColumnName": "用户", "xpack.security.management.apiKeysTitle": "API 密钥", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "取消", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "更改密码", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "当前密码", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "您输入的当前密码不正确。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新密码", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.changePasswordForm.passwordLabel": "密码", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "保存更改", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "更改 Kibana 用户的密码后,必须更新 kibana.yml 文件并重新启动 Kibana", "xpack.security.management.editRole.cancelButtonLabel": "取消", "xpack.security.management.editRole.changeAllPrivilegesLink": "(全部更改)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "隐藏", @@ -10738,10 +10714,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "创建工作区权限", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "更新全局权限", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "更新工作区权限", - "xpack.security.management.passwordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.passwordForm.passwordLabel": "密码", - "xpack.security.management.passwordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", "xpack.security.management.roles.actionsColumnName": "鎿嶄綔", "xpack.security.management.roles.cloneRoleActionName": "克隆 {roleName}", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "取消", @@ -13242,4 +13214,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index d20450f8ec47e9..08e6c90a1044ca 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -662,16 +662,7 @@ export default function alertTests({ getService }: FtrProviderContext) { } }); - /** - * Skipping due to an issue we've discovered in the `muteAll` api - * which corrupts the apiKey and causes this test to exhibit flaky behaviour. - * Failed CIs for example: - * 1. https://github.com/elastic/kibana/issues/53690 - * 2. https://github.com/elastic/kibana/issues/53683 - * - * This will be fixed and reverted in PR: https://github.com/elastic/kibana/pull/53333 - */ - it.skip(`shouldn't schedule actions when alert is muted`, async () => { + it(`shouldn't schedule actions when alert is muted`, async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); const response = await alertUtils.createAlwaysFiringAction({ @@ -761,8 +752,7 @@ export default function alertTests({ getService }: FtrProviderContext) { } }); - // Flaky: https://github.com/elastic/kibana/issues/54125 - it.skip(`should unmute all instances when unmuting an alert`, async () => { + it(`should unmute all instances when unmuting an alert`, async () => { const testStart = new Date(); const reference = alertUtils.generateReference(); const response = await alertUtils.createAlwaysFiringAction({ diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index ad4f81777e7804..2649c5d26309db 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -54,7 +54,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expectSpaceSelector: false, } ); - await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); await PageObjects.settings.navigateTo(); }); @@ -68,7 +68,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Management']); + expect(navLinks).to.eql(['Stack Management']); }); it(`allows settings to be changed`, async () => { @@ -124,7 +124,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Management']); + expect(navLinks).to.eql(['Stack Management']); }); it(`does not allow settings to be changed`, async () => { @@ -175,7 +175,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Management']); + expect(navLinks).to.eql(['Discover', 'Stack Management']); }); it(`does not allow navigation to advanced settings; redirects to Kibana home`, async () => { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index ee58be76928b3a..79bb10e0bded16 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -40,8 +40,9 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Management'); + expect(navLinks).to.contain('Stack Management'); }); it(`allows settings to be changed`, async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index e2d5efac4644cc..7c9c9f9c8c155a 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['APM', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['APM', 'Stack Management']); }); it('can navigate to APM app', async () => { @@ -109,7 +109,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['APM', 'Management']); + expect(navLinks).to.eql(['APM', 'Stack Management']); }); it('can navigate to APM app', async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts index 1ac1784e0e05db..474240b201face 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,6 +30,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('APM'); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index d0e37ec8e3f359..71c10bd8248be8 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Management']); + expect(navLinks).to.eql(['Canvas', 'Stack Management']); }); it(`landing page shows "Create new workpad" button`, async () => { @@ -142,7 +142,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Management']); + expect(navLinks).to.eql(['Canvas', 'Stack Management']); }); it(`landing page shows disabled "Create new workpad" button`, async () => { diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 28b572401892b5..5395f125bbd22b 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector', 'settings']); const appsMenu = getService('appsMenu'); describe('spaces feature controls', function() { @@ -40,6 +40,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Canvas'); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index d25fae3c4894cd..6a6e2f23785e30 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -75,7 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Stack Management']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -253,7 +253,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dashboard', 'Management']); + expect(navLinks).to.eql(['Dashboard', 'Stack Management']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index ebe08a60c25636..002ae627c488de 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -13,7 +13,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'security', + 'spaceSelector', + 'settings', + ]); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -43,6 +49,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dashboard'); }); diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index bab798dacc453b..1189fe909ca320 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -39,6 +39,7 @@ export default function({ getService, getPageObjects }) { await esArchiver.load('dashboard_view_mode'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', + pageNavigation: 'individual', }); await browser.setWindowSize(1600, 1000); @@ -199,7 +200,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mixeduser', '123456'); - if (await appsMenu.linkExists('Management')) { + if (await appsMenu.linkExists('Stack Management')) { throw new Error('Expected management nav link to not be shown'); } }); @@ -208,7 +209,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mysuperuser', '123456'); - if (!(await appsMenu.linkExists('Management'))) { + if (!(await appsMenu.linkExists('Stack Management'))) { throw new Error('Expected management nav link to be shown'); } }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 494fd71ea6f34a..9db9a913e9a4b5 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -63,7 +63,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Stack Management']); }); describe('console', () => { @@ -144,7 +144,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it(`shows 'Dev Tools' navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dev Tools', 'Management']); + expect(navLinks).to.eql(['Dev Tools', 'Stack Management']); }); describe('console', () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts index 4184d223a96864..f917792eea027f 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'security', + 'spaceSelector', + 'settings', + ]); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); const grokDebugger = getService('grokDebugger'); @@ -40,6 +46,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dev Tools'); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 1912b16d96f36c..1796858165a2ba 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -81,7 +81,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Stack Management']); }); it('shows save button', async () => { @@ -168,7 +168,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Management']); + expect(navLinks).to.eql(['Discover', 'Stack Management']); }); it(`doesn't show save button`, async () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index e6b6f28f8b92fc..c38dda536f2530 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -15,6 +15,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { 'timePicker', 'security', 'spaceSelector', + 'settings', ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -49,6 +50,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Discover'); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index a2b062e6ef84fb..37de93a0a7e910 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Stack Management']); }); it('landing page shows "Create new graph" button', async () => { @@ -127,7 +127,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Graph', 'Management']); + expect(navLinks).to.eql(['Graph', 'Stack Management']); }); it('does not show a "Create new Workspace" button', async () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts index a0b0d5bef96680..d0d0232b5a8b14 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error', 'settings']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -34,6 +34,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Graph'); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 30cdc95b38e62e..ed25816e68712b 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -70,7 +70,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Management']); + expect(navLinks).to.eql(['Stack Management']); }); it(`index pattern listing shows create button`, async () => { @@ -113,7 +113,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { } ); - await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); await PageObjects.settings.navigateTo(); }); @@ -124,7 +124,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Management']); + expect(navLinks).to.eql(['Stack Management']); }); it(`index pattern listing doesn't show create button`, async () => { @@ -176,7 +176,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Management']); + expect(navLinks).to.eql(['Discover', 'Stack Management']); }); it(`doesn't show Index Patterns in management side-nav`, async () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index 6a2b77de17f457..75020d6eab7e4a 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -40,8 +40,9 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Management'); + expect(navLinks).to.contain('Stack Management'); }); it(`index pattern listing shows create button`, async () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index 5062f094061c03..b7c5667a575065 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -61,7 +61,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Management']); + expect(navLinks).to.eql(['Metrics', 'Stack Management']); }); describe('infrastructure landing page without data', () => { @@ -174,7 +174,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Management']); + expect(navLinks).to.eql(['Metrics', 'Stack Management']); }); describe('infrastructure landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 7c2a11a542d66e..90458ef53dfc28 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -12,7 +12,13 @@ const DATE_WITH_DATA = DATES.metricsAndLogs.hosts.withData; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'infraHome', + 'security', + 'spaceSelector', + 'settings', + ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); const retry = getService('retry'); @@ -31,7 +37,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects await esArchiver.load('empty_kibana'); - await spacesService.create({ id: 'custom_space', name: 'custom_space', @@ -48,6 +53,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Metrics'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index b9634c29dac1c9..5008f93feeb015 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Management']); + expect(navLinks).to.eql(['Logs', 'Stack Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Management']); + expect(navLinks).to.eql(['Logs', 'Stack Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 6b078d2cfa71af..61a57e09f96c57 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'infraHome', + 'security', + 'spaceSelector', + 'settings', + ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -36,6 +42,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Logs'); }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index 386914b735554c..acb92b270c4a12 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -194,8 +194,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '10485760', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '10.0 MB', total_by_field_count: '37', total_over_field_count: '92', total_partition_field_count: '8', @@ -261,8 +261,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '104857600', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '100.0 MB', total_by_field_count: '994', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index d41d96e40e2bee..6a12a28e8ac490 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -60,8 +60,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '59', total_over_field_count: '0', total_partition_field_count: '58', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 296af3179ce3ee..6593dd10928b4b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -74,8 +74,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '8388608', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '8.0 MB', total_by_field_count: '25', total_over_field_count: '92', total_partition_field_count: '3', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index 7d989bc6244b81..348910a2a8f84a 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -53,8 +53,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -104,8 +104,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -155,8 +155,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -207,8 +207,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -258,8 +258,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index f6cd7b40bc7b1d..13cac36d99a1ba 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -59,8 +59,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '15728640', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '15.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index 8fb6f21c778d3f..c25c1bfe4b7318 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security']); + const PageObjects = getPageObjects(['common', 'security', 'settings']); describe('security', () => { before(async () => { @@ -94,6 +94,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); + await PageObjects.settings.setNavType('individual'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index fc94688e98811b..c633852a2da0a7 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -39,6 +39,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Machine Learning'); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index 804ad5725edfd3..ece162cbd96cc7 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Management']); + expect(navLinks).to.eql(['Maps', 'Stack Management']); }); it(`allows a map to be created`, async () => { @@ -153,7 +153,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Management']); + expect(navLinks).to.eql(['Maps', 'Stack Management']); }); it(`does not show create new button`, async () => { @@ -248,7 +248,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('does not show Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Management']); + expect(navLinks).to.eql(['Discover', 'Stack Management']); }); it(`returns a 404`, async () => { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index d985da42ab5eda..130aefb3cae2ac 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security']); + const PageObjects = getPageObjects(['common', 'security', 'settings']); describe('security', () => { before(async () => { @@ -97,6 +97,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows monitoring navlink', async () => { + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 7459b53ca4a32f..0465cbcf54541c 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); const appsMenu = getService('appsMenu'); const find = getService('find'); @@ -37,10 +37,11 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await spacesService.delete('custom_space'); }); - it('shows Stack Monitoring navlink', async () => { + it('shows Stack Monitoring navlink fail', async () => { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 45a35029ffba2e..8ab84126b2b30a 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -11,7 +11,7 @@ import { ROLES_PATH, EDIT_ROLES_PATH, CLONE_ROLES_PATH, -} from '../../../../legacy/plugins/security/public/views/management/management_urls'; +} from '../../../../plugins/security/public/management/management_urls'; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 5fed56ee79e3dd..a1517e1934a286 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -93,7 +93,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const url = parse(await browser.getCurrentUrl()); - expect(url.hash).to.eql('#/management/security/role_mappings?_g=()'); + expect(url.hash).to.eql('#/management/security/role_mappings'); }); describe('with role mappings', () => { diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 1e79c76bf83e5e..d71d197a6ea199 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -16,6 +16,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { describe('security feature controls', () => { before(async () => { await esArchiver.load('empty_kibana'); + await PageObjects.settings.setNavType('individual'); }); after(async () => { @@ -56,7 +57,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Management'); + expect(navLinks).to.contain('Stack Management'); }); it(`displays Spaces management section`, async () => { @@ -130,7 +131,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Management'); + expect(navLinks).to.contain('Stack Management'); }); it(`doesn't display Spaces management section`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index dea45f161e4510..62483a10552e34 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Management']); + expect(navLinks).to.eql(['Timelion', 'Stack Management']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Management']); + expect(navLinks).to.eql(['Timelion', 'Stack Management']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index fb203a23359bdd..7e0fe731301a64 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'timelion', + 'security', + 'spaceSelector', + 'settings', + ]); const appsMenu = getService('appsMenu'); describe('timelion', () => { @@ -38,6 +44,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Timelion'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index a004f8db66823c..4ff82484db91c4 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Management']); + expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Stack Management']); }); it('can navigate to Uptime app', async () => { @@ -115,7 +115,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Uptime', 'Management']); + expect(navLinks).to.eql(['Uptime', 'Stack Management']); }); it('can navigate to Uptime app', async () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts index 77c5b323340bf8..c3dcb1b27771fb 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,6 +30,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Uptime'); }); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index ba701da6e517dd..2ef6a381a6a30e 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -50,13 +50,11 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); }); - // flakey see https://github.com/elastic/kibana/issues/54527 - it.skip('pagination is cleared when filter criteria changes', async () => { + it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); // there should now be pagination data in the URL - const contains = await pageObjects.uptime.pageUrlContains('pagination'); - expect(contains).to.be(true); + await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -71,8 +69,7 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); await pageObjects.uptime.setStatusFilter('up'); // ensure that pagination is removed from the URL - const doesNotContain = await pageObjects.uptime.pageUrlContains('pagination'); - expect(doesNotContain).to.be(false); + await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index d55076cb0ab43b..767dbd71655672 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -75,7 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Management']); + expect(navLinks).to.eql(['Visualize', 'Stack Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -189,7 +189,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Management']); + expect(navLinks).to.eql(['Visualize', 'Stack Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index 9193862d2ba9e4..066042896c122f 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -10,7 +10,13 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'visualize', 'security', 'spaceSelector']); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'security', + 'spaceSelector', + 'settings', + ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -40,6 +46,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); + await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Visualize'); }); diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index f04f96148583fa..2ae0ea38c957bb 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'timePicker']); const uptimeService = getService('uptime'); + const retry = getService('retry'); return new (class UptimePage { public async goToUptimePageAndSetDateRange( @@ -51,8 +53,10 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo await Promise.all(monitorIdsToCheck.map(id => uptimeService.monitorPageLinkExists(id))); } - public async pageUrlContains(value: string) { - return await uptimeService.urlContains(value); + public async pageUrlContains(value: string, expected: boolean = true) { + retry.try(async () => { + expect(await uptimeService.urlContains(value)).to.eql(expected); + }); } public async changePage(direction: 'next' | 'prev') { diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index bd35f21d8f428b..0346da334d2f2d 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -344,8 +344,7 @@ export default function({ getService }: FtrProviderContext) { }); }); - // FAILING: https://github.com/elastic/kibana/issues/52969 - describe.skip('API access with missing access token document or expired refresh token.', () => { + describe('API access with missing access token document or expired refresh token.', () => { let sessionCookie: Cookie; beforeEach(async () => { diff --git a/x-pack/test/ui_capabilities/common/nav_links_builder.ts b/x-pack/test/ui_capabilities/common/nav_links_builder.ts index 8b7741469362e8..aaeb22852bcc09 100644 --- a/x-pack/test/ui_capabilities/common/nav_links_builder.ts +++ b/x-pack/test/ui_capabilities/common/nav_links_builder.ts @@ -13,7 +13,7 @@ export class NavLinksBuilder { ...features, // management isn't a first-class "feature", but it makes our life easier here to pretend like it is management: { - navLinkId: 'kibana:management', + navLinkId: 'kibana:stack_management', }, }; } diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index 4af7d81e5a7b4f..5c13e6b0eb51eb 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -68,7 +68,7 @@ export class UICapabilitiesService { : {}; const response = await this.axios.post( `${spaceUrlPrefix}/api/core/capabilities`, - { applications: [...applications, 'kibana:management'] }, + { applications: [...applications, 'kibana:stack_management'] }, { headers: requestHeaders, } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 7070e3f5aa4939..7d2933f9d92385 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -38,7 +38,8 @@ "monitoring/common/*": [ "x-pack/monitoring/common/*" ], - "plugins/*": ["src/legacy/core_plugins/*/public/"] + "plugins/*": ["src/legacy/core_plugins/*/public/"], + "fixtures/*": ["src/fixtures/*"] }, "types": [ "node", diff --git a/yarn.lock b/yarn.lock index 034a2ed08b4e68..3563ee3fc2733f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4398,6 +4398,13 @@ resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.0.tgz#cbb49815a5e1129d5f23836a98d65d93822409af" integrity sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ== +"@types/flot@^0.0.31": + version "0.0.31" + resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.31.tgz#0daca37c6c855b69a0a7e2e37dd0f84b3db8c8c1" + integrity sha512-X+RcMQCqPlQo8zPT6cUFTd/PoYBShMQlHUeOXf05jWlfYnvLuRmluB9z+2EsOKFgUzqzZve5brx+gnFxBaHEUw== + dependencies: + "@types/jquery" "*" + "@types/geojson@*": version "7946.0.7" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" @@ -4601,7 +4608,7 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae" integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog== -"@types/jquery@^3.3.31": +"@types/jquery@*", "@types/jquery@^3.3.31": version "3.3.31" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== @@ -4821,6 +4828,11 @@ resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271" integrity sha512-ShHzHkYD+Ldw3eyttptCpUhF1/mkInWwasQkCNXZHOsJMJ/UMa8wXrxSrTJaVk0r4pLK/VnESVM0wFsfQzNEKQ== +"@types/numeral@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.26.tgz#cfab9842ef9349ce714b06722940ca7ebf8a6298" + integrity sha512-DwCsRqeOWopdEsm5KLTxKVKDSDoj+pzZD1vlwu1GQJ6IF3RhjuleYlRwyRH6MJLGaf3v8wFTnC6wo3yYfz0bnA== + "@types/object-hash@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.0.tgz#b20db2074129f71829d61ff404e618c4ac3d73cf" @@ -13296,17 +13308,7 @@ fancy-log@^1.3.2: color-support "^1.1.3" time-stamp "^1.0.0" -fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" - integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-deep-equal@^3.1.1: +fast-deep-equal@^1.0.0, fast-deep-equal@^2.0.1, fast-deep-equal@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==