From 23727004835556c836ee2cdc03ae4172f4495a32 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 27 Apr 2021 17:31:16 +0300
Subject: [PATCH 01/70] [Docs] TSVB supports url drilldowns on 7.13+ (#98460)
---
docs/user/dashboard/drilldowns.asciidoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc
index cbe47f23fcbaf..d74f88babb5ce 100644
--- a/docs/user/dashboard/drilldowns.asciidoc
+++ b/docs/user/dashboard/drilldowns.asciidoc
@@ -138,7 +138,7 @@ The following panels support dashboard and URL drilldowns.
| TSVB
^| X
-^|
+^| X
| Tag Cloud
^| X
From 6c46e4107cf72a9b11bfd5b9dd106b016e361805 Mon Sep 17 00:00:00 2001
From: Kaarina Tungseth
Date: Tue, 27 Apr 2021 09:32:02 -0500
Subject: [PATCH 02/70] [DOCS] Drilldown docs changes for 7.13 (#98390)
---
docs/settings/url-drilldown-settings.asciidoc | 31 +++++
docs/setup/settings.asciidoc | 1 +
docs/user/dashboard/drilldowns.asciidoc | 125 +++++++-----------
3 files changed, 83 insertions(+), 74 deletions(-)
create mode 100644 docs/settings/url-drilldown-settings.asciidoc
diff --git a/docs/settings/url-drilldown-settings.asciidoc b/docs/settings/url-drilldown-settings.asciidoc
new file mode 100644
index 0000000000000..8be3a21bfbffc
--- /dev/null
+++ b/docs/settings/url-drilldown-settings.asciidoc
@@ -0,0 +1,31 @@
+[[url-drilldown-settings-kb]]
+=== URL drilldown settings in {kib}
+++++
+URL drilldown settings
+++++
+
+Configure the URL drilldown settings in your `kibana.yml` configuration file.
+
+[cols="2*<"]
+|===
+| [[url-drilldown-enabled]] `url_drilldown.enabled`
+ | When `true`, enables URL drilldowns on your {kib} instance.
+
+| [[external-URL-policy]] `externalUrl.policy`
+ | Configures the external URL policies. URL drilldowns respect the global *External URL* service, which you can use to deny or allow external URLs.
+By default all external URLs are allowed.
+|===
+
+For example, to allow external URLs only to the `example.com` domain with the `https` scheme, except for the `danger.example.com` sub-domain,
+which is denied even when `https` scheme is used:
+
+["source","yml"]
+-----------
+externalUrl.policy:
+ - allow: false
+ host: danger.example.com
+ - allow: true
+ host: example.com
+ protocol: https
+-----------
+
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index 1b027739169ad..0aab86fb5a9e2 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -756,3 +756,4 @@ include::{kib-repo-dir}/settings/security-settings.asciidoc[]
include::{kib-repo-dir}/settings/spaces-settings.asciidoc[]
include::{kib-repo-dir}/settings/task-manager-settings.asciidoc[]
include::{kib-repo-dir}/settings/telemetry-settings.asciidoc[]
+include::{kib-repo-dir}/settings/url-drilldown-settings.asciidoc[]
diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc
index d74f88babb5ce..fc25f84030ee2 100644
--- a/docs/user/dashboard/drilldowns.asciidoc
+++ b/docs/user/dashboard/drilldowns.asciidoc
@@ -2,8 +2,8 @@
[[drilldowns]]
== Create custom dashboard actions
-Custom dashboard actions, also known as drilldowns, allow you to create
-workflows for analyzing and troubleshooting your data. Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all of the panels. Each panel can have multiple drilldowns.
+Custom dashboard actions, or _drilldowns_, allow you to create workflows for analyzing and troubleshooting your data.
+Drilldowns apply only to the panel that you created the drilldown from, and are not shared across all panels. Each panel can have multiple drilldowns.
Third-party developers can create drilldowns. To learn how to code drilldowns, refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin].
@@ -11,27 +11,23 @@ Third-party developers can create drilldowns. To learn how to code drilldowns, r
[[supported-drilldowns]]
=== Supported drilldowns
-{kib} supports two types of drilldowns.
-
-[NOTE]
-=====================================
-Some drilldowns are paid subscription features, while others are free.
-For a comparison of the Elastic subscription levels,
-refer https://www.elastic.co/subscriptions[the subscription page].
-=====================================
+{kib} supports dashboard and URL drilldowns.
[float]
[[dashboard-drilldowns]]
==== Dashboard drilldowns
Dashboard drilldowns enable you to open a dashboard from another dashboard,
-taking the time range, filters, and other parameters with you,
+taking the time range, filters, and other parameters with you
so the context remains the same. Dashboard drilldowns help you to continue your analysis from a new perspective.
For example, if you have a dashboard that shows the overall status of multiple data center,
you can create a drilldown that navigates from the overall status dashboard to a dashboard
that shows a single data center or server.
+[role="screenshot"]
+image:images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard]
+
[float]
[[url-drilldowns]]
==== URL drilldowns
@@ -39,45 +35,25 @@ that shows a single data center or server.
URL drilldowns enable you to navigate from a dashboard to internal or external URLs.
Destination URLs can be dynamic, depending on the dashboard context or user interaction with a panel.
For example, if you have a dashboard that shows data from a Github repository, you can create a URL drilldown
-that opens Github from the dashboard.
+that opens Github from the dashboard panel.
+
+[role="screenshot"]
+image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github]
Some panels support multiple interactions, also known as triggers.
The <> you use to create a <> depends on the trigger you choose. URL drilldowns support these types of triggers:
-* *Single click* — A single data point in the visualization.
+* *Single click* — A single data point in the panel.
-* *Range selection* — A range of values in a visualization.
+* *Range selection* — A range of values in a panel.
For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`.
-To disable URL drilldowns on your {kib} instance, add the following line to `kibana.yml` config file:
-
-["source","yml"]
------------
-url_drilldown.enabled: false
------------
-
-URL drilldown also respects the global *External URL* service, which can be used to deny/allow external URLs.
-By default all external URLs are allowed. To configure external URL policies you need to use `externalUrl.policy` setting in `kibana.yml`, for example:
-
-["source","yml"]
------------
-externalUrl.policy:
- - allow: false
- host: danger.example.com
- - allow: true
- host: example.com
- protocol: https
------------
-
-The above rules allow external URLs only to `example.com` domain with `https` scheme, except for `danger.example.com` sub-domain,
-which is denied even when `https` scheme is used.
-
[float]
[[dashboard-drilldown-supported-panels]]
-=== Supported panels
+=== Supported panel types
-The following panels support dashboard and URL drilldowns.
+The following panel types support drilldowns.
[options="header"]
|===
@@ -160,25 +136,23 @@ The following panels support dashboard and URL drilldowns.
[float]
[[drilldowns-example]]
-=== Try it: Create a dashboard drilldown
+=== Create a dashboard drilldown
To create dashboard drilldowns, you create or locate the dashboards you want to connect, then configure the drilldown that allows you to easily open one dashboard from the other dashboard.
-image:images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard]
-
[float]
==== Create the dashboard
. Add the *Sample web logs* data.
-. Create a new dashboard, then add the following panels:
+. Create a new dashboard, then add the following panels from the *Visualize Library*:
* *[Logs] Heatmap*
* *[Logs] Host, Visits, and Bytes Table*
* *[Logs] Total Requests and Bytes*
* *[Logs] Visitors by OS*
+
-If you don’t see data for a panel, try changing the <>.
+If you don’t see the data on a panel, try changing the <>.
. Save the dashboard. In the *Title* field, enter `Host Overview`.
@@ -197,79 +171,82 @@ Filter: `geo.src: CN`
. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*.
-. Give the drilldown a name, then select *Go to dashboard*.
+. Click *Go to dashboard*.
-. From the *Choose a destination dashboard* dropdown, select *Host Overview*.
+.. Give the drilldown a name. For example, `My Drilldown`.
-. To carry over the filter, query, and date range, make sure that *Use filters and query from origin dashboard* and *Use date range from origin dashboard* are selected.
-+
-[role="screenshot"]
-image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination]
+.. From the *Choose a destination dashboard* dropdown, select *Host Overview*.
-. Click *Create drilldown*.
-+
-The drilldown is stored as dashboard metadata.
+.. To use the geo.src filter, KQL query, and time filter, select *Use filters and query from origin dashboard* and *Use date range from origin dashboard*.
+
+.. Click *Create drilldown*.
. Save the dashboard.
-+
-If you fail to save the dashboard, the drilldown is lost when you navigate away from the dashboard.
-. In the *[Logs] Visitors by OS* panel, click *win 8*, then select the drilldown.
+. In the *[Logs] Visitors by OS* panel, click *win 8*, then select `My Drilldown`.
+
[role="screenshot"]
image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard]
-. On the *Host Overview* dashboard, verify that the search query, filters,
-and date range are carried over.
+. On the *Host Overview* dashboard, verify that the geo.src filter, KQL query, and time filter are applied.
[float]
[[create-a-url-drilldown]]
-=== Try it: Create a URL drilldown
+=== Create a URL drilldown
To create URL drilldowns, you add <> to a URL template, which configures the behavior of the drilldown.
-image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github]
-
. Add the *Sample web logs* data.
-. Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but works for demonstration purposes.
+. Open the *[Logs] Web traffic* dashboard.
. In the toolbar, click *Edit*.
. Open the *[Logs] Visitors by OS* panel menu, then select *Create drilldown*.
-.. In the *Name* field, enter `Show on Github`.
+. Click *Go to URL*.
-.. Select *Go to URL*.
+.. Give the drilldown a name. For example, `Show on Github`.
-.. Enter the URL template:
+.. For the *Trigger*, select *Single click*.
+
+.. To navigate to the {kib} repository Github issues, enter the following in the *Enter URL* field:
+
[source, bash]
----
https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}}
----
+
-The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice.
-+
-[role="screenshot"]
-image:images/url_drilldown_url_template.png[URL template input]
+`{{event.value}}` is substituted with a value associated with a selected pie slice.
.. Click *Create drilldown*.
-+
-The drilldown is stored as dashboard metadata.
. Save the dashboard.
-+
-If you fail to save the dashboard, the drilldown is lost when you navigate away from the dashboard.
. On the *[Logs] Visitors by OS* panel, click any chart slice, then select *Show on Github*.
+
[role="screenshot"]
image:images/url_drilldown_popup.png[URL drilldown popup]
-. On the page that lists the issues in the {kib} repository, verify the slice value appears in Github.
+. In the list of {kib} repository issues, verify that the slice value appears.
+
[role="screenshot"]
image:images/url_drilldown_github.png[Github]
+[float]
+[[manage-drilldowns]]
+=== Manage drilldowns
+
+Make changes to your drilldowns, make a copy of your drilldowns for another panel, and delete drilldowns.
+
+. Open the panel menu that includes the drilldown, then click *Manage drilldowns*.
+
+. On the *Manage* tab, use the following options:
+
+* To change drilldowns, click *Edit* next to the drilldown you want to change, make your changes, then click *Save*.
+
+* To make a copy, click *Copy* next to the drilldown you want to change, enter the drilldown name, then click *Create drilldown*.
+
+* To delete a drilldown, select the drilldown you want to delete, then click *Delete*.
+
include::url-drilldown.asciidoc[]
From 18d9d435afe76b3a04b1977ea070655a365a2474 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Tue, 27 Apr 2021 16:49:20 +0200
Subject: [PATCH 03/70] [ML] Transforms: Adds a link to discover from the
transform list to the actions menu. (#97805)
Adds a link to discover from the transform list to the actions menu. Conditions for the link to be enabled:
- Kibana index pattern must be available
- Transform must have been started once and done some progress so there's the destination index available
---
x-pack/plugins/transform/kibana.json | 3 +-
.../public/app/__mocks__/app_dependencies.tsx | 19 +++-
.../transform/public/app/app_dependencies.tsx | 13 ++-
.../transform/public/app/common/index.ts | 1 -
.../public/app/common/navigation.test.tsx | 16 ---
.../public/app/common/navigation.tsx | 19 ----
.../public/app/mount_management_section.ts | 6 +-
.../step_create/step_create_form.tsx | 43 +++++++-
.../discover_action_name.test.tsx | 88 +++++++++++++++++
.../action_discover/discover_action_name.tsx | 97 ++++++++++++++++++
.../components/action_discover/index.ts | 9 ++
.../action_discover/use_action_discover.tsx | 99 +++++++++++++++++++
.../transform_list/expanded_row.test.tsx | 30 +++---
.../transform_list/use_actions.test.tsx | 14 ++-
.../components/transform_list/use_actions.tsx | 3 +
.../transform_list/use_columns.test.tsx | 7 +-
x-pack/plugins/transform/public/plugin.ts | 12 ++-
.../apps/transform/creation_index_pattern.ts | 26 +++++
.../test/functional/apps/transform/index.ts | 1 +
.../functional/services/transform/discover.ts | 65 ++++++++++++
.../functional/services/transform/index.ts | 3 +
.../services/transform/transform_table.ts | 9 +-
22 files changed, 508 insertions(+), 75 deletions(-)
delete mode 100644 x-pack/plugins/transform/public/app/common/navigation.test.tsx
create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx
create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx
create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/index.ts
create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx
create mode 100644 x-pack/test/functional/services/transform/discover.ts
diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json
index d5da9377ed870..4216ac9761e86 100644
--- a/x-pack/plugins/transform/kibana.json
+++ b/x-pack/plugins/transform/kibana.json
@@ -9,7 +9,8 @@
"licensing",
"management",
"features",
- "savedObjects"
+ "savedObjects",
+ "share"
],
"optionalPlugins": [
"security",
diff --git a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx
index 41b7482c4c0f8..5a6f8cf72e36d 100644
--- a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx
+++ b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx
@@ -7,17 +7,28 @@
import { useContext } from 'react';
+import type { ScopedHistory } from 'kibana/public';
+
import { coreMock } from '../../../../../../src/core/public/mocks';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
+import { savedObjectsPluginMock } from '../../../../../../src/plugins/saved_objects/public/mocks';
+import { SharePluginStart } from '../../../../../../src/plugins/share/public';
+
import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
+import type { AppDependencies } from '../app_dependencies';
import { MlSharedContext } from './shared_context';
+import type { GetMlSharedImportsReturnType } from '../../shared_imports';
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const dataStart = dataPluginMock.createStartContract();
-const appDependencies = {
+// Replace mock to support syntax using `.then()` as used in transform code.
+coreStart.savedObjects.client.find = jest.fn().mockResolvedValue({ savedObjects: [] });
+
+const appDependencies: AppDependencies = {
+ application: coreStart.application,
chrome: coreStart.chrome,
data: dataStart,
docLinks: coreStart.docLinks,
@@ -28,11 +39,15 @@ const appDependencies = {
storage: ({ get: jest.fn() } as unknown) as Storage,
overlays: coreStart.overlays,
http: coreSetup.http,
+ history: {} as ScopedHistory,
+ savedObjectsPlugin: savedObjectsPluginMock.createStartContract(),
+ share: ({ urlGenerators: { getUrlGenerator: jest.fn() } } as unknown) as SharePluginStart,
+ ml: {} as GetMlSharedImportsReturnType,
};
export const useAppDependencies = () => {
const ml = useContext(MlSharedContext);
- return { ...appDependencies, ml, savedObjects: jest.fn() };
+ return { ...appDependencies, ml };
};
export const useToastNotifications = () => {
diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx
index c49ab8183521f..c39aa5a49e5e9 100644
--- a/x-pack/plugins/transform/public/app/app_dependencies.tsx
+++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx
@@ -5,17 +5,19 @@
* 2.0.
*/
-import { CoreSetup, CoreStart } from 'src/core/public';
-import { DataPublicPluginStart } from 'src/plugins/data/public';
-import { SavedObjectsStart } from 'src/plugins/saved_objects/public';
-import { ScopedHistory } from 'kibana/public';
+import type { CoreSetup, CoreStart } from 'src/core/public';
+import type { DataPublicPluginStart } from 'src/plugins/data/public';
+import type { SavedObjectsStart } from 'src/plugins/saved_objects/public';
+import type { ScopedHistory } from 'kibana/public';
+import type { SharePluginStart } from 'src/plugins/share/public';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
-import { Storage } from '../../../../../src/plugins/kibana_utils/public';
+import type { Storage } from '../../../../../src/plugins/kibana_utils/public';
import type { GetMlSharedImportsReturnType } from '../shared_imports';
export interface AppDependencies {
+ application: CoreStart['application'];
chrome: CoreStart['chrome'];
data: DataPublicPluginStart;
docLinks: CoreStart['docLinks'];
@@ -28,6 +30,7 @@ export interface AppDependencies {
overlays: CoreStart['overlays'];
history: ScopedHistory;
savedObjectsPlugin: SavedObjectsStart;
+ share: SharePluginStart;
ml: GetMlSharedImportsReturnType;
}
diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts
index 8fa97139ab967..ccd90f8759358 100644
--- a/x-pack/plugins/transform/public/app/common/index.ts
+++ b/x-pack/plugins/transform/public/app/common/index.ts
@@ -28,7 +28,6 @@ export {
} from './transform';
export { TRANSFORM_LIST_COLUMN, TransformListAction, TransformListRow } from './transform_list';
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
-export { getDiscoverUrl } from './navigation';
export {
getEsAggFromAggConfig,
isPivotAggsConfigWithUiSupport,
diff --git a/x-pack/plugins/transform/public/app/common/navigation.test.tsx b/x-pack/plugins/transform/public/app/common/navigation.test.tsx
deleted file mode 100644
index af2f586873961..0000000000000
--- a/x-pack/plugins/transform/public/app/common/navigation.test.tsx
+++ /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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getDiscoverUrl } from './navigation';
-
-describe('navigation', () => {
- test('getDiscoverUrl should provide encoded url to Discover page', () => {
- expect(getDiscoverUrl('farequote-airline', 'http://example.com')).toBe(
- 'http://example.com/app/discover#?_g=()&_a=(index:farequote-airline)'
- );
- });
-});
diff --git a/x-pack/plugins/transform/public/app/common/navigation.tsx b/x-pack/plugins/transform/public/app/common/navigation.tsx
index 9daaf8c840755..b847ac66de58e 100644
--- a/x-pack/plugins/transform/public/app/common/navigation.tsx
+++ b/x-pack/plugins/transform/public/app/common/navigation.tsx
@@ -7,28 +7,9 @@
import React, { FC } from 'react';
import { Redirect } from 'react-router-dom';
-import rison from 'rison-node';
import { SECTION_SLUG } from '../constants';
-/**
- * Gets a url for navigating to Discover page.
- * @param indexPatternId Index pattern ID.
- * @param baseUrl Base url.
- */
-export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string {
- const _g = rison.encode({});
-
- // Add the index pattern ID to the appState part of the URL.
- const _a = rison.encode({
- index: indexPatternId,
- });
-
- const hash = `/discover#?_g=${_g}&_a=${_a}`;
-
- return `${baseUrl}/app${hash}`;
-}
-
export const RedirectToTransformManagement: FC = () => ;
export const RedirectToCreateTransform: FC<{ savedObjectId: string }> = ({ savedObjectId }) => (
diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts
index 019e1f56cee06..1d39d233f8284 100644
--- a/x-pack/plugins/transform/public/app/mount_management_section.ts
+++ b/x-pack/plugins/transform/public/app/mount_management_section.ts
@@ -28,8 +28,8 @@ export async function mountManagementSection(
const { http, notifications, getStartServices } = coreSetup;
const startServices = await getStartServices();
const [core, plugins] = startServices;
- const { chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core;
- const { data } = plugins;
+ const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core;
+ const { data, share } = plugins;
const { docTitle } = chrome;
// Initialize services
@@ -39,6 +39,7 @@ export async function mountManagementSection(
// AppCore/AppPlugins to be passed on as React context
const appDependencies: AppDependencies = {
+ application,
chrome,
data,
docLinks,
@@ -51,6 +52,7 @@ export async function mountManagementSection(
uiSettings,
history,
savedObjectsPlugin: plugins.savedObjects,
+ share,
ml: await getMlSharedImports(),
};
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
index 36bdca7921622..526f59e7dad41 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
@@ -26,6 +26,11 @@ import {
import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
+import {
+ DISCOVER_APP_URL_GENERATOR,
+ DiscoverUrlGeneratorState,
+} from '../../../../../../../../../src/plugins/discover/public';
+
import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms';
import {
isGetTransformsStatsResponseSchema,
@@ -36,7 +41,7 @@ import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants
import { getErrorMessage } from '../../../../../../common/utils/errors';
-import { getTransformProgress, getDiscoverUrl } from '../../../../common';
+import { getTransformProgress } from '../../../../common';
import { useApi } from '../../../../hooks/use_api';
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
import { RedirectToTransformManagement } from '../../../../common/navigation';
@@ -86,13 +91,45 @@ export const StepCreateForm: FC = React.memo(
const [progressPercentComplete, setProgressPercentComplete] = useState(
undefined
);
+ const [discoverLink, setDiscoverLink] = useState();
const deps = useAppDependencies();
const indexPatterns = deps.data.indexPatterns;
const toastNotifications = useToastNotifications();
+ const { getUrlGenerator } = deps.share.urlGenerators;
+ const isDiscoverAvailable = deps.application.capabilities.discover?.show ?? false;
useEffect(() => {
+ let unmounted = false;
+
onChange({ created, started, indexPatternId });
+
+ const getDiscoverUrl = async (): Promise => {
+ const state: DiscoverUrlGeneratorState = {
+ indexPatternId,
+ };
+
+ let discoverUrlGenerator;
+ try {
+ discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR);
+ } catch (error) {
+ // ignore error thrown when url generator is not available
+ return;
+ }
+
+ const discoverUrl = await discoverUrlGenerator.createUrl(state);
+ if (!unmounted) {
+ setDiscoverLink(discoverUrl);
+ }
+ };
+
+ if (started === true && indexPatternId !== undefined && isDiscoverAvailable) {
+ getDiscoverUrl();
+ }
+
+ return () => {
+ unmounted = true;
+ };
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [created, started, indexPatternId]);
@@ -477,7 +514,7 @@ export const StepCreateForm: FC = React.memo(
)}
- {started === true && indexPatternId !== undefined && (
+ {isDiscoverAvailable && discoverLink !== undefined && (
}
@@ -490,7 +527,7 @@ export const StepCreateForm: FC = React.memo(
defaultMessage: 'Use Discover to explore the transform.',
}
)}
- href={getDiscoverUrl(indexPatternId, deps.http.basePath.get())}
+ href={discoverLink}
data-test-subj="transformWizardCardDiscover"
/>
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx
new file mode 100644
index 0000000000000..8dba93399792c
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { cloneDeep } from 'lodash';
+import React from 'react';
+import { IntlProvider } from 'react-intl';
+
+import { render, waitFor, screen } from '@testing-library/react';
+
+import { TransformListRow } from '../../../../common';
+import { isDiscoverActionDisabled, DiscoverActionName } from './discover_action_name';
+
+import transformListRow from '../../../../common/__mocks__/transform_list_row.json';
+
+jest.mock('../../../../../shared_imports');
+jest.mock('../../../../../app/app_dependencies');
+
+// @ts-expect-error mock data is too loosely typed
+const item: TransformListRow = transformListRow;
+
+describe('Transform: Transform List Actions isDiscoverActionDisabled()', () => {
+ it('should be disabled when more than one item is passed in', () => {
+ expect(isDiscoverActionDisabled([item, item], false, true)).toBe(true);
+ });
+ it('should be disabled when forceDisable is true', () => {
+ expect(isDiscoverActionDisabled([item], true, true)).toBe(true);
+ });
+ it('should be disabled when the index pattern is not available', () => {
+ expect(isDiscoverActionDisabled([item], false, false)).toBe(true);
+ });
+ it('should be disabled when the transform started but has no index pattern', () => {
+ const itemCopy = cloneDeep(item);
+ itemCopy.stats.state = 'started';
+ expect(isDiscoverActionDisabled([itemCopy], false, false)).toBe(true);
+ });
+ it('should be enabled when the transform started and has an index pattern', () => {
+ const itemCopy = cloneDeep(item);
+ itemCopy.stats.state = 'started';
+ expect(isDiscoverActionDisabled([itemCopy], false, true)).toBe(false);
+ });
+ it('should be enabled when the index pattern is available', () => {
+ expect(isDiscoverActionDisabled([item], false, true)).toBe(false);
+ });
+});
+
+describe('Transform: Transform List Actions ', () => {
+ it('renders an enabled button', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // assert
+ await waitFor(() => {
+ expect(
+ screen.queryByTestId('transformDiscoverActionNameText disabled')
+ ).not.toBeInTheDocument();
+ expect(screen.queryByTestId('transformDiscoverActionNameText enabled')).toBeInTheDocument();
+ expect(screen.queryByText('View in Discover')).toBeInTheDocument();
+ });
+ });
+
+ it('renders a disabled button', async () => {
+ // prepare
+ const itemCopy = cloneDeep(item);
+ itemCopy.stats.checkpointing.last.checkpoint = 0;
+ render(
+
+
+
+ );
+
+ // assert
+ await waitFor(() => {
+ expect(screen.queryByTestId('transformDiscoverActionNameText disabled')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('transformDiscoverActionNameText enabled')
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText('View in Discover')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx
new file mode 100644
index 0000000000000..259bf82371dba
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiToolTip } from '@elastic/eui';
+
+import { TRANSFORM_STATE } from '../../../../../../common/constants';
+
+import { getTransformProgress, TransformListRow } from '../../../../common';
+
+export const discoverActionNameText = i18n.translate(
+ 'xpack.transform.transformList.discoverActionNameText',
+ {
+ defaultMessage: 'View in Discover',
+ }
+);
+
+export const isDiscoverActionDisabled = (
+ items: TransformListRow[],
+ forceDisable: boolean,
+ indexPatternExists: boolean
+) => {
+ if (items.length !== 1) {
+ return true;
+ }
+
+ const item = items[0];
+
+ // Disable discover action if it's a batch transform and was never started
+ const stoppedTransform = item.stats.state === TRANSFORM_STATE.STOPPED;
+ const transformProgress = getTransformProgress(item);
+ const isBatchTransform = typeof item.config.sync === 'undefined';
+ const transformNeverStarted =
+ stoppedTransform === true && transformProgress === undefined && isBatchTransform === true;
+
+ return forceDisable === true || indexPatternExists === false || transformNeverStarted === true;
+};
+
+export interface DiscoverActionNameProps {
+ indexPatternExists: boolean;
+ items: TransformListRow[];
+}
+export const DiscoverActionName: FC = ({ indexPatternExists, items }) => {
+ const isBulkAction = items.length > 1;
+
+ const item = items[0];
+
+ // Disable discover action if it's a batch transform and was never started
+ const stoppedTransform = item.stats.state === TRANSFORM_STATE.STOPPED;
+ const transformProgress = getTransformProgress(item);
+ const isBatchTransform = typeof item.config.sync === 'undefined';
+ const transformNeverStarted =
+ stoppedTransform && transformProgress === undefined && isBatchTransform === true;
+
+ let disabledTransformMessage;
+ if (isBulkAction === true) {
+ disabledTransformMessage = i18n.translate(
+ 'xpack.transform.transformList.discoverTransformBulkToolTip',
+ {
+ defaultMessage: 'Links to Discover are not supported as a bulk action.',
+ }
+ );
+ } else if (!indexPatternExists) {
+ disabledTransformMessage = i18n.translate(
+ 'xpack.transform.transformList.discoverTransformNoIndexPatternToolTip',
+ {
+ defaultMessage: `A Kibana index pattern is required for the destination index to be viewable in Discover`,
+ }
+ );
+ } else if (transformNeverStarted) {
+ disabledTransformMessage = i18n.translate(
+ 'xpack.transform.transformList.discoverTransformToolTip',
+ {
+ defaultMessage: `The transform needs to be started before it's available in Discover.`,
+ }
+ );
+ }
+
+ if (typeof disabledTransformMessage !== 'undefined') {
+ return (
+
+
+ {discoverActionNameText}
+
+
+ );
+ }
+
+ return (
+ {discoverActionNameText}
+ );
+};
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/index.ts
new file mode 100644
index 0000000000000..b8ba624faf02c
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { useDiscoverAction } from './use_action_discover';
+export { DiscoverActionName } from './discover_action_name';
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.tsx
new file mode 100644
index 0000000000000..468ed0e6b892d
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/use_action_discover.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+
+import {
+ DiscoverUrlGeneratorState,
+ DISCOVER_APP_URL_GENERATOR,
+} from '../../../../../../../../../src/plugins/discover/public';
+
+import { TransformListAction, TransformListRow } from '../../../../common';
+
+import { useSearchItems } from '../../../../hooks/use_search_items';
+import { useAppDependencies } from '../../../../app_dependencies';
+
+import {
+ isDiscoverActionDisabled,
+ discoverActionNameText,
+ DiscoverActionName,
+} from './discover_action_name';
+
+const getIndexPatternTitleFromTargetIndex = (item: TransformListRow) =>
+ Array.isArray(item.config.dest.index) ? item.config.dest.index.join(',') : item.config.dest.index;
+
+export type DiscoverAction = ReturnType;
+export const useDiscoverAction = (forceDisable: boolean) => {
+ const appDeps = useAppDependencies();
+ const savedObjectsClient = appDeps.savedObjects.client;
+ const indexPatterns = appDeps.data.indexPatterns;
+ const { getUrlGenerator } = appDeps.share.urlGenerators;
+ const isDiscoverAvailable = !!appDeps.application.capabilities.discover?.show;
+
+ const { getIndexPatternIdByTitle, loadIndexPatterns } = useSearchItems(undefined);
+
+ const [indexPatternsLoaded, setIndexPatternsLoaded] = useState(false);
+
+ useEffect(() => {
+ async function checkIndexPatternAvailability() {
+ await loadIndexPatterns(savedObjectsClient, indexPatterns);
+ setIndexPatternsLoaded(true);
+ }
+
+ checkIndexPatternAvailability();
+ }, [indexPatterns, loadIndexPatterns, savedObjectsClient]);
+
+ const clickHandler = useCallback(
+ async (item: TransformListRow) => {
+ let discoverUrlGenerator;
+ try {
+ discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR);
+ } catch (error) {
+ // ignore error thrown when url generator is not available
+ return;
+ }
+
+ const indexPatternTitle = getIndexPatternTitleFromTargetIndex(item);
+ const indexPatternId = getIndexPatternIdByTitle(indexPatternTitle);
+ const state: DiscoverUrlGeneratorState = {
+ indexPatternId,
+ };
+ const path = await discoverUrlGenerator.createUrl(state);
+ appDeps.application.navigateToApp('discover', { path });
+ },
+ [appDeps.application, getIndexPatternIdByTitle, getUrlGenerator]
+ );
+
+ const indexPatternExists = useCallback(
+ (item: TransformListRow) => {
+ const indexPatternTitle = getIndexPatternTitleFromTargetIndex(item);
+ const indexPatternId = getIndexPatternIdByTitle(indexPatternTitle);
+ return indexPatternId !== undefined;
+ },
+ [getIndexPatternIdByTitle]
+ );
+
+ const action: TransformListAction = useMemo(
+ () => ({
+ name: (item: TransformListRow) => {
+ return ;
+ },
+ available: () => isDiscoverAvailable,
+ enabled: (item: TransformListRow) =>
+ indexPatternsLoaded &&
+ !isDiscoverActionDisabled([item], forceDisable, indexPatternExists(item)),
+ description: discoverActionNameText,
+ icon: 'visTable',
+ type: 'icon',
+ onClick: clickHandler,
+ 'data-test-subj': 'transformActionDiscover',
+ }),
+ [forceDisable, indexPatternExists, indexPatternsLoaded, isDiscoverAvailable, clickHandler]
+ );
+
+ return { action };
+};
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx
index d25f8c62a4e94..77d20dc4d9078 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { render, fireEvent } from '@testing-library/react';
+import { render, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import moment from 'moment-timezone';
import { TransformListRow } from '../../../../common';
@@ -41,20 +41,26 @@ describe('Transform: Transform List ', () => {
);
- expect(getByText('Details')).toBeInTheDocument();
- expect(getByText('Stats')).toBeInTheDocument();
- expect(getByText('JSON')).toBeInTheDocument();
- expect(getByText('Messages')).toBeInTheDocument();
- expect(getByText('Preview')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(getByText('Details')).toBeInTheDocument();
+ expect(getByText('Stats')).toBeInTheDocument();
+ expect(getByText('JSON')).toBeInTheDocument();
+ expect(getByText('Messages')).toBeInTheDocument();
+ expect(getByText('Preview')).toBeInTheDocument();
- const tabContent = getByTestId('transformDetailsTabContent');
- expect(tabContent).toBeInTheDocument();
+ const tabContent = getByTestId('transformDetailsTabContent');
+ expect(tabContent).toBeInTheDocument();
- expect(getByTestId('transformDetailsTab')).toHaveAttribute('aria-selected', 'true');
- expect(within(tabContent).getByText('General')).toBeInTheDocument();
+ expect(getByTestId('transformDetailsTab')).toHaveAttribute('aria-selected', 'true');
+ expect(within(tabContent).getByText('General')).toBeInTheDocument();
+ });
fireEvent.click(getByTestId('transformStatsTab'));
- expect(getByTestId('transformStatsTab')).toHaveAttribute('aria-selected', 'true');
- expect(within(tabContent).getByText('Stats')).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(getByTestId('transformStatsTab')).toHaveAttribute('aria-selected', 'true');
+ const tabContent = getByTestId('transformDetailsTabContent');
+ expect(within(tabContent).getByText('Stats')).toBeInTheDocument();
+ });
});
});
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx
index 90487d21610ea..b7d5a2b7104ae 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx
@@ -7,20 +7,26 @@
import { renderHook } from '@testing-library/react-hooks';
-import { useActions } from './use_actions';
-
jest.mock('../../../../../shared_imports');
jest.mock('../../../../../app/app_dependencies');
+import { useActions } from './use_actions';
+
describe('Transform: Transform List Actions', () => {
- test('useActions()', () => {
- const { result } = renderHook(() => useActions({ forceDisable: false, transformNodes: 1 }));
+ test('useActions()', async () => {
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useActions({ forceDisable: false, transformNodes: 1 })
+ );
+
+ await waitForNextUpdate();
+
const actions = result.current.actions;
// Using `any` for the callback. Somehow the EUI types don't pass
// on the `data-test-subj` attribute correctly. We're interested
// in the runtime result here anyway.
expect(actions.map((a: any) => a['data-test-subj'])).toStrictEqual([
+ 'transformActionDiscover',
'transformActionStart',
'transformActionStop',
'transformActionEdit',
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx
index d9b9008490666..ddf41d356529a 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx
@@ -13,6 +13,7 @@ import { TransformListRow } from '../../../../common';
import { useCloneAction } from '../action_clone';
import { useDeleteAction, DeleteActionModal } from '../action_delete';
+import { useDiscoverAction } from '../action_discover';
import { EditTransformFlyout } from '../edit_transform_flyout';
import { useEditAction } from '../action_edit';
import { useStartAction, StartActionModal } from '../action_start';
@@ -30,6 +31,7 @@ export const useActions = ({
} => {
const cloneAction = useCloneAction(forceDisable, transformNodes);
const deleteAction = useDeleteAction(forceDisable);
+ const discoverAction = useDiscoverAction(forceDisable);
const editAction = useEditAction(forceDisable, transformNodes);
const startAction = useStartAction(forceDisable, transformNodes);
const stopAction = useStopAction(forceDisable);
@@ -45,6 +47,7 @@ export const useActions = ({
>
),
actions: [
+ discoverAction.action,
startAction.action,
stopAction.action,
editAction.action,
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx
index 53eed01f1226d..f3974430b662c 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx
@@ -13,8 +13,11 @@ jest.mock('../../../../../shared_imports');
jest.mock('../../../../../app/app_dependencies');
describe('Transform: Job List Columns', () => {
- test('useColumns()', () => {
- const { result } = renderHook(() => useColumns([], () => {}, 1, []));
+ test('useColumns()', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useColumns([], () => {}, 1, []));
+
+ await waitForNextUpdate();
+
const columns: ReturnType['columns'] = result.current.columns;
expect(columns).toHaveLength(7);
diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts
index 67abd8a7f1a78..b058be46d677b 100644
--- a/x-pack/plugins/transform/public/plugin.ts
+++ b/x-pack/plugins/transform/public/plugin.ts
@@ -7,11 +7,12 @@
import { i18n as kbnI18n } from '@kbn/i18n';
-import { CoreSetup } from 'src/core/public';
-import { DataPublicPluginStart } from 'src/plugins/data/public';
-import { HomePublicPluginSetup } from 'src/plugins/home/public';
-import { SavedObjectsStart } from 'src/plugins/saved_objects/public';
-import { ManagementSetup } from '../../../../src/plugins/management/public';
+import type { CoreSetup } from 'src/core/public';
+import type { DataPublicPluginStart } from 'src/plugins/data/public';
+import type { HomePublicPluginSetup } from 'src/plugins/home/public';
+import type { SavedObjectsStart } from 'src/plugins/saved_objects/public';
+import type { ManagementSetup } from 'src/plugins/management/public';
+import type { SharePluginStart } from 'src/plugins/share/public';
import { registerFeature } from './register_feature';
export interface PluginsDependencies {
@@ -19,6 +20,7 @@ export interface PluginsDependencies {
management: ManagementSetup;
home: HomePublicPluginSetup;
savedObjects: SavedObjectsStart;
+ share: SharePluginStart;
}
export class TransformUiPlugin {
diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts
index a720aec6bb478..61579ac68ae53 100644
--- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts
+++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts
@@ -89,6 +89,7 @@ export default function ({ getService }: FtrProviderContext) {
get destinationIndex(): string {
return `user-${this.transformId}`;
},
+ discoverAdjustSuperDatePicker: true,
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "category.keyword": {'],
pivotAdvancedEditorValue: {
@@ -210,6 +211,7 @@ export default function ({ getService }: FtrProviderContext) {
],
},
],
+ discoverQueryHits: '7,270',
},
} as PivotTransformTestData,
{
@@ -247,6 +249,7 @@ export default function ({ getService }: FtrProviderContext) {
get destinationIndex(): string {
return `user-${this.transformId}`;
},
+ discoverAdjustSuperDatePicker: false,
expected: {
pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "geoip.country_iso_code": {'],
pivotAdvancedEditorValue: {
@@ -294,6 +297,7 @@ export default function ({ getService }: FtrProviderContext) {
rows: 5,
},
histogramCharts: [],
+ discoverQueryHits: '10',
},
} as PivotTransformTestData,
{
@@ -317,6 +321,7 @@ export default function ({ getService }: FtrProviderContext) {
get destinationIndex(): string {
return `user-${this.transformId}`;
},
+ discoverAdjustSuperDatePicker: true,
expected: {
latestPreview: {
column: 0,
@@ -342,6 +347,7 @@ export default function ({ getService }: FtrProviderContext) {
'July 12th 2019, 23:31:12',
],
},
+ discoverQueryHits: '10',
},
} as LatestTransformTestData,
];
@@ -533,6 +539,26 @@ export default function ({ getService }: FtrProviderContext) {
progress: testData.expected.row.progress,
});
});
+
+ it('navigates to discover and displays results of the destination index', async () => {
+ await transform.testExecution.logTestStep('should show the actions popover');
+ await transform.table.assertTransformRowActions(testData.transformId, false);
+
+ await transform.testExecution.logTestStep('should navigate to discover');
+ await transform.table.clickTransformRowAction('Discover');
+
+ if (testData.discoverAdjustSuperDatePicker) {
+ await transform.discover.assertNoResults(testData.destinationIndex);
+ await transform.testExecution.logTestStep(
+ 'should switch quick select lookback to years'
+ );
+ await transform.discover.assertSuperDatePickerToggleQuickMenuButtonExists();
+ await transform.discover.openSuperDatePicker();
+ await transform.discover.quickSelectYears();
+ }
+
+ await transform.discover.assertDiscoverQueryHits(testData.expected.discoverQueryHits);
+ });
});
}
});
diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts
index 89ac903b16d01..ca82459c47f2f 100644
--- a/x-pack/test/functional/apps/transform/index.ts
+++ b/x-pack/test/functional/apps/transform/index.ts
@@ -66,6 +66,7 @@ export interface BaseTransformTestData {
transformDescription: string;
expected: any;
destinationIndex: string;
+ discoverAdjustSuperDatePicker: boolean;
}
export interface PivotTransformTestData extends BaseTransformTestData {
diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts
new file mode 100644
index 0000000000000..a98f7e5ae9890
--- /dev/null
+++ b/x-pack/test/functional/services/transform/discover.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export function TransformDiscoverProvider({ getService }: FtrProviderContext) {
+ const find = getService('find');
+ const testSubjects = getService('testSubjects');
+
+ return {
+ async assertDiscoverQueryHits(expectedDiscoverQueryHits: string) {
+ await testSubjects.existOrFail('discoverQueryHits');
+
+ const actualDiscoverQueryHits = await testSubjects.getVisibleText('discoverQueryHits');
+
+ expect(actualDiscoverQueryHits).to.eql(
+ expectedDiscoverQueryHits,
+ `Discover query hits should be ${expectedDiscoverQueryHits}, got ${actualDiscoverQueryHits}`
+ );
+ },
+
+ async assertNoResults(expectedDestinationIndex: string) {
+ // Discover should use the destination index pattern
+ const actualIndexPatternSwitchLinkText = await (
+ await testSubjects.find('indexPattern-switch-link')
+ ).getVisibleText();
+ expect(actualIndexPatternSwitchLinkText).to.eql(
+ expectedDestinationIndex,
+ `Destination index should be ${expectedDestinationIndex}, got ${actualIndexPatternSwitchLinkText}`
+ );
+
+ await testSubjects.existOrFail('discoverNoResults');
+ },
+
+ async assertSuperDatePickerToggleQuickMenuButtonExists() {
+ await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton');
+ },
+
+ async openSuperDatePicker() {
+ await testSubjects.click('superDatePickerToggleQuickMenuButton');
+ await testSubjects.existOrFail('superDatePickerQuickMenu');
+ },
+
+ async quickSelectYears() {
+ const quickMenuElement = await testSubjects.find('superDatePickerQuickMenu');
+
+ // No test subject, select "Years" to look back 15 years instead of 15 minutes.
+ await find.selectValue(`[aria-label*="Time unit"]`, 'y');
+
+ // Apply
+ const applyButton = await quickMenuElement.findByClassName('euiQuickSelect__applyButton');
+ const actualApplyButtonText = await applyButton.getVisibleText();
+ expect(actualApplyButtonText).to.be('Apply');
+
+ await applyButton.click();
+ await testSubjects.existOrFail('discoverQueryHits');
+ },
+ };
+}
diff --git a/x-pack/test/functional/services/transform/index.ts b/x-pack/test/functional/services/transform/index.ts
index 36265fb9369d3..c9179cc307aaf 100644
--- a/x-pack/test/functional/services/transform/index.ts
+++ b/x-pack/test/functional/services/transform/index.ts
@@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import { TransformAPIProvider } from './api';
import { TransformEditFlyoutProvider } from './edit_flyout';
+import { TransformDiscoverProvider } from './discover';
import { TransformManagementProvider } from './management';
import { TransformNavigationProvider } from './navigation';
import { TransformSecurityCommonProvider } from './security_common';
@@ -22,6 +23,7 @@ import { MachineLearningTestResourcesProvider } from '../ml/test_resources';
export function TransformProvider(context: FtrProviderContext) {
const api = TransformAPIProvider(context);
+ const discover = TransformDiscoverProvider(context);
const editFlyout = TransformEditFlyoutProvider(context);
const management = TransformManagementProvider(context);
const navigation = TransformNavigationProvider(context);
@@ -35,6 +37,7 @@ export function TransformProvider(context: FtrProviderContext) {
return {
api,
+ discover,
editFlyout,
management,
navigation,
diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts
index 17d4e56e0cdf9..cafaa2606f255 100644
--- a/x-pack/test/functional/services/transform/transform_table.ts
+++ b/x-pack/test/functional/services/transform/transform_table.ts
@@ -9,6 +9,8 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
+type TransformRowActionName = 'Clone' | 'Delete' | 'Edit' | 'Start' | 'Stop' | 'Discover';
+
export function TransformTableProvider({ getService }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
@@ -238,6 +240,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
await testSubjects.existOrFail('transformActionClone');
await testSubjects.existOrFail('transformActionDelete');
+ await testSubjects.existOrFail('transformActionDiscover');
await testSubjects.existOrFail('transformActionEdit');
if (isTransformRunning) {
@@ -251,7 +254,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
public async assertTransformRowActionEnabled(
transformId: string,
- action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit',
+ action: TransformRowActionName,
expectedValue: boolean
) {
const selector = `transformAction${action}`;
@@ -274,7 +277,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
public async clickTransformRowActionWithRetry(
transformId: string,
- action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit'
+ action: TransformRowActionName
) {
await retry.tryForTime(30 * 1000, async () => {
await browser.pressKeys(browser.keys.ESCAPE);
@@ -285,7 +288,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
});
}
- public async clickTransformRowAction(action: string) {
+ public async clickTransformRowAction(action: TransformRowActionName) {
await testSubjects.click(`transformAction${action}`);
}
From 8de766904d28da27ad707a920ade9510e86a2941 Mon Sep 17 00:00:00 2001
From: Andrea Del Rio
Date: Tue, 27 Apr 2021 09:12:17 -0700
Subject: [PATCH 04/70] [K8] Small fixes (#98099)
---
.../sidebar/discover_field_search.tsx | 4 +-
.../public/lib/panel/_embeddable_panel.scss | 5 +-
.../public/top_nav_menu/_index.scss | 10 +++-
.../solution_toolbar/items/button.scss | 5 ++
.../solution_toolbar/items/quick_group.scss | 6 +++
.../home/components/filter_list_button.tsx | 46 ++++++++++---------
6 files changed, 49 insertions(+), 27 deletions(-)
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx
index 67dda6dd0e9a8..e11c1716efe6b 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx
@@ -233,7 +233,7 @@ export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }:
const footer = () => {
return (
-
+
-
+
{i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', {
defaultMessage: 'Filter by type',
})}
diff --git a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
index f7ee1f3c741c4..9072c26576097 100644
--- a/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
+++ b/src/plugins/embeddable/public/lib/panel/_embeddable_panel.scss
@@ -120,9 +120,10 @@
// EDITING MODE
.embPanel--editing {
- border-style: dashed;
- border-color: $euiColorMediumShade;
+ border-style: dashed !important;
+ border-color: $euiColorMediumShade !important;
transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
+ border-width: $euiBorderWidthThin;
&:hover,
&:focus {
diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss
index bc27cf061eb68..9af1bb5434bb1 100644
--- a/src/plugins/navigation/public/top_nav_menu/_index.scss
+++ b/src/plugins/navigation/public/top_nav_menu/_index.scss
@@ -1,5 +1,13 @@
.kbnTopNavMenu {
- margin-right: $euiSizeXS;
+ @include kbnThemeStyle('v7') {
+ margin-right: $euiSizeXS;
+ }
+
+ @include kbnThemeStyle('v8') {
+ button:last-child {
+ margin-right: 0;
+ }
+ }
}
.kbnTopNavMenu__badgeWrapper {
diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss
index b8022201acf59..4fc3651ee9f73 100644
--- a/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss
+++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/button.scss
@@ -4,4 +4,9 @@
// Lighten the border color for all states
border-color: $euiBorderColor !important; // sass-lint:disable-line no-important
+
+ @include kbnThemeStyle('v8') {
+ border-width: $euiBorderWidthThin;
+ border-style: solid;
+ }
}
diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss
index 870a9a945ed5d..876ee659b71d7 100644
--- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss
+++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss
@@ -1,6 +1,12 @@
.quickButtonGroup {
.quickButtonGroup__button {
background-color: $euiColorEmptyShade;
+ @include kbnThemeStyle('v8') {
+ // sass-lint:disable-block no-important
+ border-width: $euiBorderWidthThin !important;
+ border-style: solid !important;
+ border-color: $euiBorderColor !important;
+ }
}
// Temporary fix for two tone icons to make them monochrome
diff --git a/x-pack/plugins/index_management/public/application/sections/home/components/filter_list_button.tsx b/x-pack/plugins/index_management/public/application/sections/home/components/filter_list_button.tsx
index 44f565f98cdb0..4bd9a01380c0e 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/components/filter_list_button.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/components/filter_list_button.tsx
@@ -7,7 +7,7 @@
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFilterButton, EuiPopover, EuiFilterSelectItem } from '@elastic/eui';
+import { EuiFilterButton, EuiFilterGroup, EuiPopover, EuiFilterSelectItem } from '@elastic/eui';
interface Filter {
name: string;
@@ -65,26 +65,28 @@ export function FilterListButton({ onChange, filters }: Props<
);
return (
-
-
- {Object.entries(filters).map(([filter, item], index) => (
- toggleFilter(filter as T)}
- data-test-subj="filterItem"
- >
- {(item as Filter).name}
-
- ))}
-
-
+
+
+
+ {Object.entries(filters).map(([filter, item], index) => (
+ toggleFilter(filter as T)}
+ data-test-subj="filterItem"
+ >
+ {(item as Filter).name}
+
+ ))}
+
+
+
);
}
From aa281ffad7c0b1808154e00b937251f36e2ff76a Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Tue, 27 Apr 2021 09:36:27 -0700
Subject: [PATCH 05/70] [Metrics UI] Unskip Home Page Functional Test (#98085)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../test/functional/apps/infra/home_page.ts | 48 +------------------
x-pack/test/functional/apps/infra/index.ts | 20 ++++----
x-pack/test/functional/apps/infra/link_to.ts | 2 +-
3 files changed, 14 insertions(+), 56 deletions(-)
diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts
index a5b8e69833fb6..1cc7c87f3a1a8 100644
--- a/x-pack/test/functional/apps/infra/home_page.ts
+++ b/x-pack/test/functional/apps/infra/home_page.ts
@@ -5,24 +5,17 @@
* 2.0.
*/
-import expect from '@kbn/expect/expect.js';
import { FtrProviderContext } from '../../ftr_provider_context';
import { DATES } from './constants';
const DATE_WITH_DATA = DATES.metricsAndLogs.hosts.withData;
const DATE_WITHOUT_DATA = DATES.metricsAndLogs.hosts.withoutData;
-const COMMON_REQUEST_HEADERS = {
- 'kbn-xsrf': 'some-xsrf-token',
-};
-
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const pageObjects = getPageObjects(['common', 'infraHome']);
- const supertest = getService('supertest');
- // FLAKY: https://github.com/elastic/kibana/issues/75724
- describe.skip('Home page', function () {
+ describe('Home page', function () {
this.tags('includeFirefox');
before(async () => {
await esArchiver.load('empty_kibana');
@@ -54,45 +47,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await pageObjects.infraHome.goToTime(DATE_WITHOUT_DATA);
await pageObjects.infraHome.getNoMetricsDataPrompt();
});
-
- it('records telemetry for hosts', async () => {
- await pageObjects.infraHome.goToTime(DATE_WITH_DATA);
- await pageObjects.infraHome.getWaffleMap();
-
- const resp = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
- .set(COMMON_REQUEST_HEADERS)
- .set('Accept', 'application/json')
- .send({
- unencrypted: true,
- })
- .expect(200)
- .then((res: any) => res.body);
-
- expect(
- resp[0].stack_stats.kibana.plugins.infraops.last_24_hours.hits.infraops_hosts
- ).to.be.greaterThan(0);
- });
-
- it('records telemetry for docker', async () => {
- await pageObjects.infraHome.goToTime(DATE_WITH_DATA);
- await pageObjects.infraHome.getWaffleMap();
- await pageObjects.infraHome.goToDocker();
-
- const resp = await supertest
- .post(`/api/telemetry/v2/clusters/_stats`)
- .set(COMMON_REQUEST_HEADERS)
- .set('Accept', 'application/json')
- .send({
- unencrypted: true,
- })
- .expect(200)
- .then((res: any) => res.body);
-
- expect(
- resp[0].stack_stats.kibana.plugins.infraops.last_24_hours.hits.infraops_docker
- ).to.be.greaterThan(0);
- });
});
});
};
diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts
index 9c828253245d0..8cdcf6b619b26 100644
--- a/x-pack/test/functional/apps/infra/index.ts
+++ b/x-pack/test/functional/apps/infra/index.ts
@@ -8,15 +8,19 @@
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
- describe('InfraOps app', function () {
+ describe('InfraOps App', function () {
this.tags('ciGroup7');
- loadTestFile(require.resolve('./metrics_anomalies'));
- loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./feature_controls'));
- loadTestFile(require.resolve('./log_entry_categories_tab'));
- loadTestFile(require.resolve('./log_entry_rate_tab'));
- loadTestFile(require.resolve('./logs_source_configuration'));
- loadTestFile(require.resolve('./metrics_source_configuration'));
- loadTestFile(require.resolve('./link_to'));
+ describe('Metrics UI', function () {
+ loadTestFile(require.resolve('./home_page'));
+ loadTestFile(require.resolve('./metrics_source_configuration'));
+ loadTestFile(require.resolve('./metrics_anomalies'));
+ });
+ describe('Logs UI', function () {
+ loadTestFile(require.resolve('./log_entry_categories_tab'));
+ loadTestFile(require.resolve('./log_entry_rate_tab'));
+ loadTestFile(require.resolve('./logs_source_configuration'));
+ loadTestFile(require.resolve('./link_to'));
+ });
});
};
diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts
index b7a554cea311f..3e070fb9849b1 100644
--- a/x-pack/test/functional/apps/infra/link_to.ts
+++ b/x-pack/test/functional/apps/infra/link_to.ts
@@ -22,7 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const traceId = '433b4651687e18be2c6c8e3b11f53d09';
- describe('Infra link-to', function () {
+ describe('link-to Logs', function () {
it('redirects to the logs app and parses URL search params correctly', async () => {
const location = {
hash: '',
From 52a90e3dc9a19e7c211d13fd5c672634b3f51526 Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Tue, 27 Apr 2021 13:04:08 -0400
Subject: [PATCH 06/70] [Fleet] Use default port for fleet server cloud url
(#98492)
---
.../fleet/server/services/settings.test.ts | 18 +++++++++++++++++-
.../plugins/fleet/server/services/settings.ts | 6 +++++-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/fleet/server/services/settings.test.ts b/x-pack/plugins/fleet/server/services/settings.test.ts
index a9f9600addc39..87b3e163c1bb3 100644
--- a/x-pack/plugins/fleet/server/services/settings.test.ts
+++ b/x-pack/plugins/fleet/server/services/settings.test.ts
@@ -17,7 +17,7 @@ describe('getCloudFleetServersHosts', () => {
expect(getCloudFleetServersHosts()).toBeUndefined();
});
- it('should return fleet server hosts if cloud is correctly setup', () => {
+ it('should return fleet server hosts if cloud is correctly setup with default port == 443', () => {
mockedAppContextService.getCloud.mockReturnValue({
cloudId:
'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==',
@@ -32,4 +32,20 @@ describe('getCloudFleetServersHosts', () => {
]
`);
});
+
+ it('should return fleet server hosts if cloud is correctly setup with a default port', () => {
+ mockedAppContextService.getCloud.mockReturnValue({
+ cloudId:
+ 'test:dGVzdC5mcjo5MjQzJGRhM2I2YjNkYWY5ZDRjODE4ZjI4ZmEzNDdjMzgzODViJDgxMmY4NWMxZjNjZTQ2YTliYjgxZjFjMWIxMzRjNmRl',
+ isCloudEnabled: true,
+ deploymentId: 'deployment-id-1',
+ apm: {},
+ });
+
+ expect(getCloudFleetServersHosts()).toMatchInlineSnapshot(`
+ Array [
+ "https://deployment-id-1.fleet.test.fr:9243",
+ ]
+ `);
+ });
});
diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts
index 4ef9a3a95cbd0..2046e2571c926 100644
--- a/x-pack/plugins/fleet/server/services/settings.ts
+++ b/x-pack/plugins/fleet/server/services/settings.ts
@@ -84,6 +84,10 @@ export function getCloudFleetServersHosts() {
}
// Fleet Server url are formed like this `https://.fleet.
- return [`https://${cloudSetup.deploymentId}.fleet.${res.host}`];
+ return [
+ `https://${cloudSetup.deploymentId}.fleet.${res.host}${
+ res.defaultPort !== '443' ? `:${res.defaultPort}` : ''
+ }`,
+ ];
}
}
From 808959e316082718c1c1081c362fbc6c91f2fdbc Mon Sep 17 00:00:00 2001
From: Aleh Zasypkin
Date: Tue, 27 Apr 2021 19:09:54 +0200
Subject: [PATCH 07/70] Handle `401 Unauthorized` errors in a more
user-friendly way (#94927)
---
.eslintrc.js | 4 +
test/common/services/security/user.ts | 30 +-
test/scripts/jenkins_xpack_build_plugins.sh | 1 +
.../plugins/reporting/server/routes/jobs.ts | 2 +
x-pack/plugins/security/common/constants.ts | 15 +
.../capture_url/capture_url_app.test.ts | 63 ++-
.../capture_url/capture_url_app.ts | 45 +-
.../authentication/login/components/index.ts | 2 +-
.../login/components/login_form/index.ts | 2 +-
.../components/login_form/login_form.test.tsx | 6 +-
.../components/login_form/login_form.tsx | 14 +-
.../authentication/login/login_page.test.tsx | 9 +-
.../authentication/login/login_page.tsx | 34 +-
.../__snapshots__/prompt_page.test.tsx.snap | 5 +
.../unauthenticated_page.test.tsx.snap | 3 +
.../authentication_service.test.mocks.ts | 9 +
.../authentication_service.test.ts | 458 ++++++++++++++++--
.../authentication/authentication_service.ts | 72 ++-
.../authentication/authenticator.test.ts | 72 ++-
.../server/authentication/authenticator.ts | 41 +-
.../can_redirect_request.test.ts | 30 ++
.../authentication/can_redirect_request.ts | 7 +-
.../authentication/providers/base.mock.ts | 1 +
.../server/authentication/providers/base.ts | 4 +
.../authentication/providers/oidc.test.ts | 76 ++-
.../server/authentication/providers/oidc.ts | 54 ++-
.../authentication/providers/saml.test.ts | 63 ++-
.../server/authentication/providers/saml.ts | 55 ++-
.../security/server/authentication/tokens.ts | 10 +-
.../unauthenticated_page.test.tsx | 35 ++
.../authentication/unauthenticated_page.tsx | 55 +++
.../reset_session_page.test.tsx.snap | 2 +-
.../authorization/authorization_service.tsx | 24 +-
.../authorization/reset_session_page.test.tsx | 10 +-
.../authorization/reset_session_page.tsx | 120 ++---
x-pack/plugins/security/server/index.ts | 1 +
x-pack/plugins/security/server/plugin.ts | 7 +-
.../security/server/prompt_page.test.tsx | 57 +++
.../plugins/security/server/prompt_page.tsx | 96 ++++
.../routes/authentication/common.test.ts | 6 +-
.../server/routes/authentication/common.ts | 3 +-
.../server/routes/authentication/oidc.ts | 17 +-
.../server/routes/authentication/saml.test.ts | 7 +-
.../server/routes/authentication/saml.ts | 16 +-
x-pack/plugins/security/server/routes/tags.ts | 27 ++
.../server/routes/views/capture_url.test.ts | 42 +-
.../server/routes/views/capture_url.ts | 6 +-
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../apis/security/basic_login.js | 4 +-
.../test/functional/apps/security/security.ts | 2 +-
.../tests/anonymous/login.ts | 20 +-
.../tests/kerberos/kerberos_login.ts | 14 +-
.../login_selector/basic_functionality.ts | 61 ++-
.../oidc/authorization_code_flow/oidc_auth.ts | 157 +++---
.../tests/oidc/implicit_flow/oidc_auth.ts | 18 +-
.../tests/pki/pki_auth.ts | 16 +-
.../tests/saml/saml_login.ts | 161 +++---
.../common/test_endpoints/kibana.json | 7 +
.../common/test_endpoints/public/index.ts | 9 +
.../common/test_endpoints/public/plugin.tsx | 29 ++
.../common/test_endpoints/server/index.ts | 15 +
.../test_endpoints/server/init_routes.ts | 38 ++
.../login_selector.config.ts | 3 +
.../test/security_functional/oidc.config.ts | 3 +
.../test/security_functional/saml.config.ts | 3 +
.../login_selector/basic_functionality.ts | 60 +++
.../tests/oidc/url_capture.ts | 21 +
.../tests/saml/url_capture.ts | 21 +
69 files changed, 1837 insertions(+), 545 deletions(-)
create mode 100644 x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
create mode 100644 x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
create mode 100644 x-pack/plugins/security/server/authentication/authentication_service.test.mocks.ts
create mode 100644 x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx
create mode 100644 x-pack/plugins/security/server/authentication/unauthenticated_page.tsx
create mode 100644 x-pack/plugins/security/server/prompt_page.test.tsx
create mode 100644 x-pack/plugins/security/server/prompt_page.tsx
create mode 100644 x-pack/plugins/security/server/routes/tags.ts
create mode 100644 x-pack/test/security_functional/fixtures/common/test_endpoints/kibana.json
create mode 100644 x-pack/test/security_functional/fixtures/common/test_endpoints/public/index.ts
create mode 100644 x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx
create mode 100644 x-pack/test/security_functional/fixtures/common/test_endpoints/server/index.ts
create mode 100644 x-pack/test/security_functional/fixtures/common/test_endpoints/server/init_routes.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index 19ba7cacc3c44..0f7af42fafbfd 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1377,6 +1377,10 @@ module.exports = {
['parent', 'sibling', 'index'],
],
pathGroups: [
+ {
+ pattern: '{**,.}/*.test.mocks',
+ group: 'unknown',
+ },
{
pattern: '{@kbn/**,src/**,kibana{,/**}}',
group: 'internal',
diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts
index 3bd31bb5ed186..d6813105ecbf6 100644
--- a/test/common/services/security/user.ts
+++ b/test/common/services/security/user.ts
@@ -33,7 +33,7 @@ export class User {
public async delete(username: string) {
this.log.debug(`deleting user ${username}`);
- const { data, status, statusText } = await await this.kbnClient.request({
+ const { data, status, statusText } = await this.kbnClient.request({
path: `/internal/security/users/${username}`,
method: 'DELETE',
});
@@ -44,4 +44,32 @@ export class User {
}
this.log.debug(`deleted user ${username}`);
}
+
+ public async disable(username: string) {
+ this.log.debug(`disabling user ${username}`);
+ const { data, status, statusText } = await this.kbnClient.request({
+ path: `/internal/security/users/${encodeURIComponent(username)}/_disable`,
+ method: 'POST',
+ });
+ if (status !== 204) {
+ throw new Error(
+ `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`
+ );
+ }
+ this.log.debug(`disabled user ${username}`);
+ }
+
+ public async enable(username: string) {
+ this.log.debug(`enabling user ${username}`);
+ const { data, status, statusText } = await this.kbnClient.request({
+ path: `/internal/security/users/${encodeURIComponent(username)}/_enable`,
+ method: 'POST',
+ });
+ if (status !== 204) {
+ throw new Error(
+ `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`
+ );
+ }
+ this.log.debug(`enabled user ${username}`);
+ }
}
diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh
index 496964983cc6c..cb0b5ec1d56da 100755
--- a/test/scripts/jenkins_xpack_build_plugins.sh
+++ b/test/scripts/jenkins_xpack_build_plugins.sh
@@ -13,6 +13,7 @@ node scripts/build_kibana_platform_plugins \
--scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \
--scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \
--scan-dir "$XPACK_DIR/test/usage_collection/plugins" \
+ --scan-dir "$XPACK_DIR/test/security_functional/fixtures/common" \
--scan-dir "$KIBANA_DIR/examples" \
--scan-dir "$XPACK_DIR/examples" \
--workers 12 \
diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts
index 7141b1a141185..3f2a95a34224c 100644
--- a/x-pack/plugins/reporting/server/routes/jobs.ts
+++ b/x-pack/plugins/reporting/server/routes/jobs.ts
@@ -7,6 +7,7 @@
import { schema } from '@kbn/config-schema';
import Boom from '@hapi/boom';
+import { ROUTE_TAG_CAN_REDIRECT } from '../../../security/server';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
@@ -198,6 +199,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
docId: schema.string({ minLength: 3 }),
}),
},
+ options: { tags: [ROUTE_TAG_CAN_REDIRECT] },
},
userHandler(async (user, context, req, res) => {
// ensure the async dependencies are loaded
diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts
index a205109f537e7..ef83230fc2aba 100644
--- a/x-pack/plugins/security/common/constants.ts
+++ b/x-pack/plugins/security/common/constants.ts
@@ -19,7 +19,22 @@ export const GLOBAL_RESOURCE = '*';
export const APPLICATION_PREFIX = 'kibana-';
export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*';
+/**
+ * This is the key of a query parameter that contains the name of the authentication provider that should be used to
+ * authenticate request. It's also used while the user is being redirected during single-sign-on authentication flows.
+ * That query parameter is discarded after the authentication flow succeeds. See the `Authenticator`,
+ * `OIDCAuthenticationProvider`, and `SAMLAuthenticationProvider` classes for more information.
+ */
export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint';
+
+/**
+ * This is the key of a query parameter that contains metadata about the (client-side) URL hash while the user is being
+ * redirected during single-sign-on authentication flows. That query parameter is discarded after the authentication
+ * flow succeeds. See the `Authenticator`, `OIDCAuthenticationProvider`, and `SAMLAuthenticationProvider` classes for
+ * more information.
+ */
+export const AUTH_URL_HASH_QUERY_STRING_PARAMETER = 'auth_url_hash';
+
export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider';
export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg';
export const NEXT_URL_QUERY_STRING_PARAMETER = 'next';
diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts
index bf68d9f7a6e5e..44fd5ab195341 100644
--- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts
+++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts
@@ -5,19 +5,29 @@
* 2.0.
*/
-import type { AppMount, ScopedHistory } from 'src/core/public';
-import { coreMock, scopedHistoryMock } from 'src/core/public/mocks';
+import { coreMock } from 'src/core/public/mocks';
import { captureURLApp } from './capture_url_app';
describe('captureURLApp', () => {
+ let mockLocationReplace: jest.Mock;
beforeAll(() => {
+ mockLocationReplace = jest.fn();
Object.defineProperty(window, 'location', {
- value: { href: 'https://some-host' },
+ value: {
+ href: 'https://some-host',
+ hash: '#/?_g=()',
+ origin: 'https://some-host',
+ replace: mockLocationReplace,
+ },
writable: true,
});
});
+ beforeEach(() => {
+ mockLocationReplace.mockClear();
+ });
+
it('properly registers application', () => {
const coreSetupMock = coreMock.createSetup();
@@ -42,34 +52,37 @@ describe('captureURLApp', () => {
it('properly handles captured URL', async () => {
window.location.href = `https://host.com/mock-base-path/internal/security/capture-url?next=${encodeURIComponent(
- '/mock-base-path/app/home'
- )}&providerType=saml&providerName=saml1#/?_g=()`;
+ '/mock-base-path/app/home?auth_provider_hint=saml1'
+ )}#/?_g=()`;
const coreSetupMock = coreMock.createSetup();
- coreSetupMock.http.post.mockResolvedValue({ location: '/mock-base-path/app/home#/?_g=()' });
-
captureURLApp.create(coreSetupMock);
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
- await (mount as AppMount)({
- element: document.createElement('div'),
- appBasePath: '',
- onAppLeave: jest.fn(),
- setHeaderActionMenu: jest.fn(),
- history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
- });
+ await mount(coreMock.createAppMountParamters());
- expect(coreSetupMock.http.post).toHaveBeenCalledTimes(1);
- expect(coreSetupMock.http.post).toHaveBeenCalledWith('/internal/security/login', {
- body: JSON.stringify({
- providerType: 'saml',
- providerName: 'saml1',
- currentURL: `https://host.com/mock-base-path/internal/security/capture-url?next=${encodeURIComponent(
- '/mock-base-path/app/home'
- )}&providerType=saml&providerName=saml1#/?_g=()`,
- }),
- });
+ expect(mockLocationReplace).toHaveBeenCalledTimes(1);
+ expect(mockLocationReplace).toHaveBeenCalledWith(
+ 'https://some-host/mock-base-path/app/home?auth_provider_hint=saml1&auth_url_hash=%23%2F%3F_g%3D%28%29#/?_g=()'
+ );
+ expect(coreSetupMock.fatalErrors.add).not.toHaveBeenCalled();
+ });
- expect(window.location.href).toBe('/mock-base-path/app/home#/?_g=()');
+ it('properly handles open redirects', async () => {
+ window.location.href = `https://host.com/mock-base-path/internal/security/capture-url?next=${encodeURIComponent(
+ 'https://evil.com/mock-base-path/app/home?auth_provider_hint=saml1'
+ )}#/?_g=()`;
+
+ const coreSetupMock = coreMock.createSetup();
+ captureURLApp.create(coreSetupMock);
+
+ const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
+ await mount(coreMock.createAppMountParamters());
+
+ expect(mockLocationReplace).toHaveBeenCalledTimes(1);
+ expect(mockLocationReplace).toHaveBeenCalledWith(
+ 'https://some-host/?auth_url_hash=%23%2F%3F_g%3D%28%29'
+ );
+ expect(coreSetupMock.fatalErrors.add).not.toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts
index 7797ce4e62102..af45314c5bacb 100644
--- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts
+++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
-import { parse } from 'url';
-
import type { ApplicationSetup, FatalErrorsSetup, HttpSetup } from 'src/core/public';
+import { AUTH_URL_HASH_QUERY_STRING_PARAMETER } from '../../../common/constants';
+import { parseNext } from '../../../common/parse_next';
+
interface CreateDeps {
application: ApplicationSetup;
http: HttpSetup;
@@ -22,20 +23,17 @@ interface CreateDeps {
* path segment into the `next` query string parameter (so that it's not lost during redirect). And
* since browsers preserve hash fragments during redirects (assuming redirect location doesn't
* specify its own hash fragment, which is true in our case) this app can capture both path and
- * hash URL segments and send them back to the authentication provider via login endpoint.
+ * hash URL segments and re-try request sending hash fragment in a dedicated query string parameter.
*
* The flow can look like this:
- * 1. User visits `/app/kibana#/management/elasticsearch` that initiates authentication.
- * 2. Provider redirect user to `/internal/security/capture-url?next=%2Fapp%2Fkibana&providerType=saml&providerName=saml1`.
- * 3. Browser preserves hash segment and users ends up at `/internal/security/capture-url?next=%2Fapp%2Fkibana&providerType=saml&providerName=saml1#/management/elasticsearch`.
- * 4. The app captures full URL and sends it back as is via login endpoint:
- * {
- * providerType: 'saml',
- * providerName: 'saml1',
- * currentURL: 'https://kibana.com/internal/security/capture-url?next=%2Fapp%2Fkibana&providerType=saml&providerName=saml1#/management/elasticsearch'
- * }
- * 5. Login endpoint handler parses and validates `next` parameter, joins it with the hash segment
- * and finally passes it to the provider that initiated capturing.
+ * 1. User visits `https://kibana.com/app/kibana#/management/elasticsearch` that initiates authentication.
+ * 2. Provider redirect user to `/internal/security/capture-url?next=%2Fapp%2Fkibana&auth_provider_hint=saml1`.
+ * 3. Browser preserves hash segment and users ends up at `/internal/security/capture-url?next=%2Fapp%2Fkibana&auth_provider_hint=saml1#/management/elasticsearch`.
+ * 4. The app reconstructs original URL, adds `auth_url_hash` query string parameter with the captured hash fragment and redirects user to:
+ * https://kibana.com/app/kibana?auth_provider_hint=saml1&auth_url_hash=%23%2Fmanagement%2Felasticsearch#/management/elasticsearch
+ * 5. Once Kibana receives this request, it immediately picks exactly the same provider to handle authentication (based on `auth_provider_hint=saml1`),
+ * and, since it has full URL now (original request path, query string and hash extracted from `auth_url_hash=%23%2Fmanagement%2Felasticsearch`),
+ * it can proceed to a proper authentication handshake.
*/
export const captureURLApp = Object.freeze({
id: 'security_capture_url',
@@ -48,19 +46,14 @@ export const captureURLApp = Object.freeze({
appRoute: '/internal/security/capture-url',
async mount() {
try {
- const { providerName, providerType } = parse(window.location.href, true).query ?? {};
- if (!providerName || !providerType) {
- fatalErrors.add(new Error('Provider to capture URL for is not specified.'));
- return () => {};
- }
-
- const { location } = await http.post<{ location: string }>('/internal/security/login', {
- body: JSON.stringify({ providerType, providerName, currentURL: window.location.href }),
- });
-
- window.location.href = location;
+ const url = new URL(
+ parseNext(window.location.href, http.basePath.serverBasePath),
+ window.location.origin
+ );
+ url.searchParams.append(AUTH_URL_HASH_QUERY_STRING_PARAMETER, window.location.hash);
+ window.location.replace(url.toString());
} catch (err) {
- fatalErrors.add(new Error('Cannot login with captured URL.'));
+ fatalErrors.add(new Error(`Cannot parse current URL: ${err && err.message}.`));
}
return () => {};
diff --git a/x-pack/plugins/security/public/authentication/login/components/index.ts b/x-pack/plugins/security/public/authentication/login/components/index.ts
index 66e91a390784a..63928e4e82e37 100644
--- a/x-pack/plugins/security/public/authentication/login/components/index.ts
+++ b/x-pack/plugins/security/public/authentication/login/components/index.ts
@@ -5,5 +5,5 @@
* 2.0.
*/
-export { LoginForm } from './login_form';
+export { LoginForm, LoginFormMessageType } from './login_form';
export { DisabledLoginForm } from './disabled_login_form';
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts b/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts
index 6215f4e1e5b7a..d12ea30c784cb 100644
--- a/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { LoginForm } from './login_form';
+export { LoginForm, MessageType as LoginFormMessageType } from './login_form';
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx
index f58150d4580b8..e816fa032a0e5 100644
--- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx
@@ -14,7 +14,7 @@ import ReactMarkdown from 'react-markdown';
import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest';
import { coreMock } from 'src/core/public/mocks';
-import { LoginForm, PageMode } from './login_form';
+import { LoginForm, MessageType, PageMode } from './login_form';
function expectPageMode(wrapper: ReactWrapper, mode: PageMode) {
const assertions: Array<[string, boolean]> =
@@ -90,7 +90,7 @@ describe('LoginForm', () => {
{
});
expect(wrapper.find(EuiCallOut).props().title).toEqual(
- `Invalid username or password. Please try again.`
+ `Username or password is incorrect. Please try again.`
);
});
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx
index ca573ada36d22..df131e2eac133 100644
--- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx
@@ -40,7 +40,7 @@ interface Props {
http: HttpStart;
notifications: NotificationsStart;
selector: LoginSelector;
- infoMessage?: string;
+ message?: { type: MessageType.Danger | MessageType.Info; content: string };
loginAssistanceMessage: string;
loginHelp?: string;
authProviderHint?: string;
@@ -66,7 +66,7 @@ enum LoadingStateType {
AutoLogin,
}
-enum MessageType {
+export enum MessageType {
None,
Info,
Danger,
@@ -106,9 +106,7 @@ export class LoginForm extends Component {
loadingState: { type: LoadingStateType.None },
username: '',
password: '',
- message: this.props.infoMessage
- ? { type: MessageType.Info, content: this.props.infoMessage }
- : { type: MessageType.None },
+ message: this.props.message || { type: MessageType.None },
mode,
previousMode: mode,
};
@@ -206,7 +204,7 @@ export class LoginForm extends Component {
>
@@ -480,8 +478,8 @@ export class LoginForm extends Component {
const message =
(error as IHttpFetchError).response?.status === 401
? i18n.translate(
- 'xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage',
- { defaultMessage: 'Invalid username or password. Please try again.' }
+ 'xpack.security.login.basicLoginForm.usernameOrPasswordIsIncorrectErrorMessage',
+ { defaultMessage: 'Username or password is incorrect. Please try again.' }
)
: i18n.translate('xpack.security.login.basicLoginForm.unknownErrorMessage', {
defaultMessage: 'Oops! Error. Try again.',
diff --git a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx
index a9596aff3bf0e..b3e2fac3ea2cc 100644
--- a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx
+++ b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx
@@ -14,7 +14,7 @@ import { coreMock } from 'src/core/public/mocks';
import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants';
import type { LoginState } from '../../../common/login_state';
-import { DisabledLoginForm, LoginForm } from './components';
+import { DisabledLoginForm, LoginForm, LoginFormMessageType } from './components';
import { LoginPage } from './login_page';
const createLoginState = (options?: Partial) => {
@@ -228,9 +228,12 @@ describe('LoginPage', () => {
resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot
});
- const { authProviderHint, infoMessage } = wrapper.find(LoginForm).props();
+ const { authProviderHint, message } = wrapper.find(LoginForm).props();
expect(authProviderHint).toBe('basic1');
- expect(infoMessage).toBe('Your session has timed out. Please log in again.');
+ expect(message).toEqual({
+ type: LoginFormMessageType.Info,
+ content: 'Your session has timed out. Please log in again.',
+ });
});
it('renders as expected when loginAssistanceMessage is set', async () => {
diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx
index 562adec7918d3..40438ac1c78f3 100644
--- a/x-pack/plugins/security/public/authentication/login/login_page.tsx
+++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx
@@ -23,7 +23,7 @@ import {
LOGOUT_REASON_QUERY_STRING_PARAMETER,
} from '../../../common/constants';
import type { LoginState } from '../../../common/login_state';
-import { DisabledLoginForm, LoginForm } from './components';
+import { DisabledLoginForm, LoginForm, LoginFormMessageType } from './components';
interface Props {
http: HttpStart;
@@ -36,18 +36,34 @@ interface State {
loginState: LoginState | null;
}
-const infoMessageMap = new Map([
+const messageMap = new Map([
[
'SESSION_EXPIRED',
- i18n.translate('xpack.security.login.sessionExpiredDescription', {
- defaultMessage: 'Your session has timed out. Please log in again.',
- }),
+ {
+ type: LoginFormMessageType.Info,
+ content: i18n.translate('xpack.security.login.sessionExpiredDescription', {
+ defaultMessage: 'Your session has timed out. Please log in again.',
+ }),
+ },
],
[
'LOGGED_OUT',
- i18n.translate('xpack.security.login.loggedOutDescription', {
- defaultMessage: 'You have logged out of Elastic.',
- }),
+ {
+ type: LoginFormMessageType.Info,
+ content: i18n.translate('xpack.security.login.loggedOutDescription', {
+ defaultMessage: 'You have logged out of Elastic.',
+ }),
+ },
+ ],
+ [
+ 'UNAUTHENTICATED',
+ {
+ type: LoginFormMessageType.Danger,
+ content: i18n.translate('xpack.security.unauthenticated.errorDescription', {
+ defaultMessage:
+ "We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.",
+ }),
+ },
],
]);
@@ -226,7 +242,7 @@ export class LoginPage extends Component {
notifications={this.props.notifications}
selector={selector}
// @ts-expect-error Map.get is ok with getting `undefined`
- infoMessage={infoMessageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())}
+ message={messageMap.get(query[LOGOUT_REASON_QUERY_STRING_PARAMETER]?.toString())}
loginAssistanceMessage={this.props.loginAssistanceMessage}
loginHelp={loginHelp}
authProviderHint={query[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]?.toString()}
diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
new file mode 100644
index 0000000000000..bcb97538b4f05
--- /dev/null
+++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PromptPage renders as expected with additional scripts 1`] = `"Elastic MockedFonts "`;
+
+exports[`PromptPage renders as expected without additional scripts 1`] = `"Elastic MockedFonts "`;
diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
new file mode 100644
index 0000000000000..55168401992f7
--- /dev/null
+++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UnauthenticatedPage renders as expected 1`] = `"Elastic MockedFonts
We couldn't log you in
We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.
"`;
diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.mocks.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.mocks.ts
new file mode 100644
index 0000000000000..12a63134f4ef2
--- /dev/null
+++ b/x-pack/plugins/security/server/authentication/authentication_service.test.mocks.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const mockCanRedirectRequest = jest.fn();
+jest.mock('./can_redirect_request', () => ({ canRedirectRequest: mockCanRedirectRequest }));
diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts
index b0be9445c3fc3..d38f963a60c33 100644
--- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts
+++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts
@@ -6,6 +6,9 @@
*/
jest.mock('./authenticator');
+jest.mock('./unauthenticated_page');
+
+import { mockCanRedirectRequest } from './authentication_service.test.mocks';
import Boom from '@hapi/boom';
@@ -18,6 +21,7 @@ import type {
KibanaRequest,
Logger,
LoggerFactory,
+ OnPreResponseToolkit,
} from 'src/core/server';
import {
coreMock,
@@ -37,6 +41,7 @@ import type { ConfigType } from '../config';
import { ConfigSchema, createConfig } from '../config';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
+import { ROUTE_TAG_AUTH_FLOW } from '../routes/tags';
import type { Session } from '../session_management';
import { sessionMock } from '../session_management/session.mock';
import { AuthenticationResult } from './authentication_result';
@@ -47,15 +52,60 @@ describe('AuthenticationService', () => {
let logger: jest.Mocked;
let mockSetupAuthenticationParams: {
http: jest.Mocked;
+ config: ConfigType;
license: jest.Mocked;
+ buildNumber: number;
+ };
+ let mockStartAuthenticationParams: {
+ legacyAuditLogger: jest.Mocked;
+ audit: jest.Mocked;
+ config: ConfigType;
+ loggers: LoggerFactory;
+ http: jest.Mocked;
+ clusterClient: ReturnType;
+ featureUsageService: jest.Mocked;
+ session: jest.Mocked>;
};
beforeEach(() => {
logger = loggingSystemMock.createLogger();
+ const httpMock = coreMock.createSetup().http;
+ (httpMock.basePath.prepend as jest.Mock).mockImplementation(
+ (path) => `${httpMock.basePath.serverBasePath}${path}`
+ );
+ (httpMock.basePath.get as jest.Mock).mockImplementation(() => httpMock.basePath.serverBasePath);
mockSetupAuthenticationParams = {
- http: coreMock.createSetup().http,
+ http: httpMock,
+ config: createConfig(ConfigSchema.validate({}), loggingSystemMock.create().get(), {
+ isTLSEnabled: false,
+ }),
license: licenseMock.create(),
+ buildNumber: 100500,
};
+ mockCanRedirectRequest.mockReturnValue(false);
+
+ const coreStart = coreMock.createStart();
+ mockStartAuthenticationParams = {
+ legacyAuditLogger: securityAuditLoggerMock.create(),
+ audit: auditServiceMock.create(),
+ config: createConfig(
+ ConfigSchema.validate({
+ encryptionKey: 'ab'.repeat(16),
+ secureCookies: true,
+ cookieName: 'my-sid-cookie',
+ }),
+ loggingSystemMock.create().get(),
+ { isTLSEnabled: false }
+ ),
+ http: coreStart.http,
+ clusterClient: elasticsearchServiceMock.createClusterClient(),
+ loggers: loggingSystemMock.create(),
+ featureUsageService: securityFeatureUsageServiceMock.createStartContract(),
+ session: sessionMock.create(),
+ };
+ (mockStartAuthenticationParams.http.basePath.get as jest.Mock).mockImplementation(
+ () => mockStartAuthenticationParams.http.basePath.serverBasePath
+ );
service = new AuthenticationService(logger);
});
@@ -71,40 +121,19 @@ describe('AuthenticationService', () => {
expect.any(Function)
);
});
+
+ it('properly registers onPreResponse handler', () => {
+ service.setup(mockSetupAuthenticationParams);
+
+ expect(mockSetupAuthenticationParams.http.registerOnPreResponse).toHaveBeenCalledTimes(1);
+ expect(mockSetupAuthenticationParams.http.registerOnPreResponse).toHaveBeenCalledWith(
+ expect.any(Function)
+ );
+ });
});
describe('#start()', () => {
- let mockStartAuthenticationParams: {
- legacyAuditLogger: jest.Mocked;
- audit: jest.Mocked;
- config: ConfigType;
- loggers: LoggerFactory;
- http: jest.Mocked;
- clusterClient: ReturnType;
- featureUsageService: jest.Mocked;
- session: jest.Mocked>;
- };
beforeEach(() => {
- const coreStart = coreMock.createStart();
- mockStartAuthenticationParams = {
- legacyAuditLogger: securityAuditLoggerMock.create(),
- audit: auditServiceMock.create(),
- config: createConfig(
- ConfigSchema.validate({
- encryptionKey: 'ab'.repeat(16),
- secureCookies: true,
- cookieName: 'my-sid-cookie',
- }),
- loggingSystemMock.create().get(),
- { isTLSEnabled: false }
- ),
- http: coreStart.http,
- clusterClient: elasticsearchServiceMock.createClusterClient(),
- loggers: loggingSystemMock.create(),
- featureUsageService: securityFeatureUsageServiceMock.createStartContract(),
- session: sessionMock.create(),
- };
-
service.setup(mockSetupAuthenticationParams);
});
@@ -318,4 +347,371 @@ describe('AuthenticationService', () => {
});
});
});
+
+ describe('onPreResponse handler', () => {
+ function getService({ runStart = true }: { runStart?: boolean } = {}) {
+ service.setup(mockSetupAuthenticationParams);
+
+ if (runStart) {
+ service.start(mockStartAuthenticationParams);
+ }
+
+ const onPreResponseHandler =
+ mockSetupAuthenticationParams.http.registerOnPreResponse.mock.calls[0][0];
+ const [authenticator] = jest.requireMock('./authenticator').Authenticator.mock.instances;
+
+ return { authenticator, onPreResponseHandler };
+ }
+
+ it('ignores responses with non-401 status code', async () => {
+ const mockReturnedValue = { type: 'next' as any };
+ const mockOnPreResponseToolkit = httpServiceMock.createOnPreResponseToolkit();
+ mockOnPreResponseToolkit.next.mockReturnValue(mockReturnedValue);
+
+ const { onPreResponseHandler } = getService();
+ for (const statusCode of [200, 400, 403, 404]) {
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest(),
+ { statusCode },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+ }
+ });
+
+ it('ignores responses to requests that cannot handle redirects', async () => {
+ const mockReturnedValue = { type: 'next' as any };
+ const mockOnPreResponseToolkit = httpServiceMock.createOnPreResponseToolkit();
+ mockOnPreResponseToolkit.next.mockReturnValue(mockReturnedValue);
+ mockCanRedirectRequest.mockReturnValue(false);
+
+ const { onPreResponseHandler } = getService();
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest(),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+ });
+
+ it('ignores responses if authenticator is not initialized', async () => {
+ // Run `setup`, but not `start` to simulate non-initialized `Authenticator`.
+ const { onPreResponseHandler } = getService({ runStart: false });
+
+ const mockReturnedValue = { type: 'next' as any };
+ const mockOnPreResponseToolkit = httpServiceMock.createOnPreResponseToolkit();
+ mockOnPreResponseToolkit.next.mockReturnValue(mockReturnedValue);
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest(),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+ });
+
+ describe('when login form is available', () => {
+ let mockReturnedValue: { type: any; body: string };
+ let mockOnPreResponseToolkit: jest.Mocked;
+ beforeEach(() => {
+ mockReturnedValue = { type: 'render' as any, body: 'body' };
+ mockOnPreResponseToolkit = httpServiceMock.createOnPreResponseToolkit();
+ mockOnPreResponseToolkit.render.mockReturnValue(mockReturnedValue);
+ });
+
+ it('redirects to the login page when user does not have an active session', async () => {
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({ path: '/app/some', query: { param: 'one two' } }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ Refresh:
+ '0;url=/mock-server-basepath/login?msg=UNAUTHENTICATED&next=%2Fmock-server-basepath%2Fapp%2Fsome',
+ },
+ });
+ });
+
+ it('performs logout if user has an active session', async () => {
+ mockStartAuthenticationParams.session.getSID.mockResolvedValue('some-sid');
+
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({ path: '/app/some', query: { param: 'one two' } }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ Refresh:
+ '0;url=/mock-server-basepath/logout?msg=UNAUTHENTICATED&next=%2Fmock-server-basepath%2Fapp%2Fsome',
+ },
+ });
+ });
+
+ it('does not preserve path for the authentication flow paths', async () => {
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({
+ path: '/api/security/saml/callback',
+ query: { param: 'one two' },
+ routeTags: [ROUTE_TAG_AUTH_FLOW],
+ }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ Refresh:
+ '0;url=/mock-server-basepath/login?msg=UNAUTHENTICATED&next=%2Fmock-server-basepath%2F',
+ },
+ });
+ });
+ });
+
+ describe('when login selector is available', () => {
+ let mockReturnedValue: { type: any; body: string };
+ let mockOnPreResponseToolkit: jest.Mocked;
+ beforeEach(() => {
+ mockReturnedValue = { type: 'render' as any, body: 'body' };
+ mockOnPreResponseToolkit = httpServiceMock.createOnPreResponseToolkit();
+ mockOnPreResponseToolkit.render.mockReturnValue(mockReturnedValue);
+
+ mockSetupAuthenticationParams.config = createConfig(
+ ConfigSchema.validate({
+ authc: {
+ providers: {
+ saml: { saml1: { order: 0, realm: 'saml1' } },
+ basic: { basic1: { order: 1 } },
+ },
+ },
+ }),
+ loggingSystemMock.create().get(),
+ { isTLSEnabled: false }
+ );
+ });
+
+ it('redirects to the login page when user does not have an active session', async () => {
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({ path: '/app/some', query: { param: 'one two' } }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ Refresh:
+ '0;url=/mock-server-basepath/login?msg=UNAUTHENTICATED&next=%2Fmock-server-basepath%2Fapp%2Fsome',
+ },
+ });
+ });
+
+ it('performs logout if user has an active session', async () => {
+ mockStartAuthenticationParams.session.getSID.mockResolvedValue('some-sid');
+
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({ path: '/app/some', query: { param: 'one two' } }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ Refresh:
+ '0;url=/mock-server-basepath/logout?msg=UNAUTHENTICATED&next=%2Fmock-server-basepath%2Fapp%2Fsome',
+ },
+ });
+ });
+
+ it('does not preserve path for the authentication flow paths', async () => {
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({
+ path: '/api/security/saml/callback',
+ query: { param: 'one two' },
+ routeTags: [ROUTE_TAG_AUTH_FLOW],
+ }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ Refresh:
+ '0;url=/mock-server-basepath/login?msg=UNAUTHENTICATED&next=%2Fmock-server-basepath%2F',
+ },
+ });
+ });
+ });
+
+ describe('when neither login selector nor login form is available', () => {
+ let mockReturnedValue: { type: any; body: string };
+ let mockOnPreResponseToolkit: jest.Mocked;
+ beforeEach(() => {
+ mockReturnedValue = { type: 'render' as any, body: 'body' };
+ mockOnPreResponseToolkit = httpServiceMock.createOnPreResponseToolkit();
+ mockOnPreResponseToolkit.render.mockReturnValue(mockReturnedValue);
+
+ mockSetupAuthenticationParams.config = createConfig(
+ ConfigSchema.validate({
+ authc: { providers: { saml: { saml1: { order: 0, realm: 'saml1' } } } },
+ }),
+ loggingSystemMock.create().get(),
+ { isTLSEnabled: false }
+ );
+ });
+
+ it('renders unauthenticated page if user does not have an active session', async () => {
+ const mockRenderUnauthorizedPage = jest
+ .requireMock('./unauthenticated_page')
+ .renderUnauthenticatedPage.mockReturnValue('rendered-view');
+
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({ path: '/app/some', query: { param: 'one two' } }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: 'rendered-view',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ },
+ });
+
+ expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({
+ basePath: mockSetupAuthenticationParams.http.basePath,
+ buildNumber: 100500,
+ originalURL: '/mock-server-basepath/app/some',
+ });
+ });
+
+ it('renders unauthenticated page if user has an active session', async () => {
+ const mockRenderUnauthorizedPage = jest
+ .requireMock('./unauthenticated_page')
+ .renderUnauthenticatedPage.mockReturnValue('rendered-view');
+ mockStartAuthenticationParams.session.getSID.mockResolvedValue('some-sid');
+
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({ path: '/app/some', query: { param: 'one two' } }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: 'rendered-view',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ },
+ });
+
+ expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({
+ basePath: mockSetupAuthenticationParams.http.basePath,
+ buildNumber: 100500,
+ originalURL: '/mock-server-basepath/app/some',
+ });
+ });
+
+ it('does not preserve path for the authentication flow paths', async () => {
+ const mockRenderUnauthorizedPage = jest
+ .requireMock('./unauthenticated_page')
+ .renderUnauthenticatedPage.mockReturnValue('rendered-view');
+
+ const { authenticator, onPreResponseHandler } = getService();
+ authenticator.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/app/some');
+ mockCanRedirectRequest.mockReturnValue(true);
+
+ await expect(
+ onPreResponseHandler(
+ httpServerMock.createKibanaRequest({
+ path: '/api/security/saml/callback',
+ query: { param: 'one two' },
+ routeTags: [ROUTE_TAG_AUTH_FLOW],
+ }),
+ { statusCode: 401 },
+ mockOnPreResponseToolkit
+ )
+ ).resolves.toBe(mockReturnedValue);
+
+ expect(mockOnPreResponseToolkit.render).toHaveBeenCalledWith({
+ body: 'rendered-view',
+ headers: {
+ 'Content-Security-Policy': `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`,
+ },
+ });
+
+ expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({
+ basePath: mockSetupAuthenticationParams.http.basePath,
+ buildNumber: 100500,
+ originalURL: '/mock-server-basepath/',
+ });
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts
index 7feeff7a5d8ed..e5895422e7a74 100644
--- a/x-pack/plugins/security/server/authentication/authentication_service.ts
+++ b/x-pack/plugins/security/server/authentication/authentication_service.ts
@@ -15,22 +15,29 @@ import type {
LoggerFactory,
} from 'src/core/server';
+import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../common/constants';
import type { SecurityLicense } from '../../common/licensing';
import type { AuthenticatedUser } from '../../common/model';
+import { shouldProviderUseLoginForm } from '../../common/model';
import type { AuditServiceSetup, SecurityAuditLogger } from '../audit';
import type { ConfigType } from '../config';
-import { getErrorStatusCode } from '../errors';
+import { getDetailedErrorMessage, getErrorStatusCode } from '../errors';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
+import { ROUTE_TAG_AUTH_FLOW } from '../routes/tags';
import type { Session } from '../session_management';
import { APIKeys } from './api_keys';
import type { AuthenticationResult } from './authentication_result';
import type { ProviderLoginAttempt } from './authenticator';
import { Authenticator } from './authenticator';
+import { canRedirectRequest } from './can_redirect_request';
import type { DeauthenticationResult } from './deauthentication_result';
+import { renderUnauthenticatedPage } from './unauthenticated_page';
interface AuthenticationServiceSetupParams {
- http: Pick;
+ http: Pick;
+ config: ConfigType;
license: SecurityLicense;
+ buildNumber: number;
}
interface AuthenticationServiceStartParams {
@@ -62,12 +69,23 @@ export interface AuthenticationServiceStart {
export class AuthenticationService {
private license!: SecurityLicense;
private authenticator?: Authenticator;
+ private session?: PublicMethodsOf;
constructor(private readonly logger: Logger) {}
- setup({ http, license }: AuthenticationServiceSetupParams) {
+ setup({ config, http, license, buildNumber }: AuthenticationServiceSetupParams) {
this.license = license;
+ // If we cannot automatically authenticate users we should redirect them straight to the login
+ // page if possible, so that they can try other methods to log in. If not possible, we should
+ // render a dedicated `Unauthenticated` page from which users can explicitly trigger a new
+ // login attempt. There are two cases when we can redirect to the login page:
+ // 1. Login selector is enabled
+ // 2. Login selector is disabled, but the provider with the lowest `order` uses login form
+ const isLoginPageAvailable =
+ config.authc.selector.enabled ||
+ shouldProviderUseLoginForm(config.authc.sortedProviders[0].type);
+
http.registerAuth(async (request, response, t) => {
if (!license.isLicenseAvailable()) {
this.logger.error('License is not available, authentication is not possible.');
@@ -118,8 +136,9 @@ export class AuthenticationService {
}
if (authenticationResult.failed()) {
- this.logger.info(`Authentication attempt failed: ${authenticationResult.error!.message}`);
const error = authenticationResult.error!;
+ this.logger.info(`Authentication attempt failed: ${getDetailedErrorMessage(error)}`);
+
// proxy Elasticsearch "native" errors
const statusCode = getErrorStatusCode(error);
if (typeof statusCode === 'number') {
@@ -139,7 +158,49 @@ export class AuthenticationService {
return t.notHandled();
});
- this.logger.debug('Successfully registered core authentication handler.');
+ http.registerOnPreResponse(async (request, preResponse, toolkit) => {
+ if (preResponse.statusCode !== 401 || !canRedirectRequest(request)) {
+ return toolkit.next();
+ }
+
+ if (!this.authenticator) {
+ // Core doesn't allow returning error here.
+ this.logger.error('Authentication sub-system is not fully initialized yet.');
+ return toolkit.next();
+ }
+
+ // If users can eventually re-login we want to redirect them directly to the page they tried
+ // to access initially, but we only want to do that for routes that aren't part of the various
+ // authentication flows that wouldn't make any sense after successful authentication.
+ const originalURL = !request.route.options.tags.includes(ROUTE_TAG_AUTH_FLOW)
+ ? this.authenticator.getRequestOriginalURL(request)
+ : `${http.basePath.get(request)}/`;
+ if (!isLoginPageAvailable) {
+ return toolkit.render({
+ body: renderUnauthenticatedPage({ buildNumber, basePath: http.basePath, originalURL }),
+ headers: { 'Content-Security-Policy': http.csp.header },
+ });
+ }
+
+ const needsToLogout = (await this.session?.getSID(request)) !== undefined;
+ if (needsToLogout) {
+ this.logger.warn('Could not authenticate user with the existing session. Forcing logout.');
+ }
+
+ return toolkit.render({
+ body: '
',
+ headers: {
+ 'Content-Security-Policy': http.csp.header,
+ Refresh: `0;url=${http.basePath.prepend(
+ `${
+ needsToLogout ? '/logout' : '/login'
+ }?msg=UNAUTHENTICATED&${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
+ originalURL
+ )}`
+ )}`,
+ },
+ });
+ });
}
start({
@@ -161,6 +222,7 @@ export class AuthenticationService {
const getCurrentUser = (request: KibanaRequest) =>
http.auth.get(request).state ?? null;
+ this.session = session;
this.authenticator = new Authenticator({
legacyAuditLogger,
audit,
diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts
index 1bd430d0c5c98..ca33be92e9e99 100644
--- a/x-pack/plugins/security/server/authentication/authenticator.test.ts
+++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts
@@ -20,6 +20,10 @@ import {
loggingSystemMock,
} from 'src/core/server/mocks';
+import {
+ AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
+} from '../../common/constants';
import type { SecurityLicenseFeatures } from '../../common/licensing';
import { licenseMock } from '../../common/licensing/index.mock';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
@@ -1780,13 +1784,13 @@ describe('Authenticator', () => {
);
});
- it('returns `notHandled` if session does not exist.', async () => {
+ it('redirects to login form if session does not exist.', async () => {
const request = httpServerMock.createKibanaRequest();
mockOptions.session.get.mockResolvedValue(null);
mockBasicAuthenticationProvider.logout.mockResolvedValue(DeauthenticationResult.notHandled());
await expect(authenticator.logout(request)).resolves.toEqual(
- DeauthenticationResult.notHandled()
+ DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
);
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
@@ -1843,12 +1847,12 @@ describe('Authenticator', () => {
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
});
- it('returns `notHandled` if session does not exist and provider name is invalid', async () => {
+ it('redirects to login form if session does not exist and provider name is invalid', async () => {
const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } });
mockOptions.session.get.mockResolvedValue(null);
await expect(authenticator.logout(request)).resolves.toEqual(
- DeauthenticationResult.notHandled()
+ DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
);
expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled();
@@ -1937,4 +1941,64 @@ describe('Authenticator', () => {
);
});
});
+
+ describe('`getRequestOriginalURL` method', () => {
+ let authenticator: Authenticator;
+ let mockOptions: ReturnType;
+ beforeEach(() => {
+ mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
+ authenticator = new Authenticator(mockOptions);
+ });
+
+ it('filters out auth specific query parameters', () => {
+ expect(authenticator.getRequestOriginalURL(httpServerMock.createKibanaRequest())).toBe(
+ '/mock-server-basepath/path'
+ );
+
+ expect(
+ authenticator.getRequestOriginalURL(
+ httpServerMock.createKibanaRequest({
+ query: {
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'saml1',
+ },
+ })
+ )
+ ).toBe('/mock-server-basepath/path');
+
+ expect(
+ authenticator.getRequestOriginalURL(
+ httpServerMock.createKibanaRequest({
+ query: {
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'saml1',
+ [AUTH_URL_HASH_QUERY_STRING_PARAMETER]: '#some-hash',
+ },
+ })
+ )
+ ).toBe('/mock-server-basepath/path');
+ });
+
+ it('allows to include additional query parameters', () => {
+ expect(
+ authenticator.getRequestOriginalURL(httpServerMock.createKibanaRequest(), [
+ ['some-param', 'some-value'],
+ ['some-param2', 'some-value2'],
+ ])
+ ).toBe('/mock-server-basepath/path?some-param=some-value&some-param2=some-value2');
+
+ expect(
+ authenticator.getRequestOriginalURL(
+ httpServerMock.createKibanaRequest({
+ query: {
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'saml1',
+ [AUTH_URL_HASH_QUERY_STRING_PARAMETER]: '#some-hash',
+ },
+ }),
+ [
+ ['some-param', 'some-value'],
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, 'oidc1'],
+ ]
+ )
+ ).toBe('/mock-server-basepath/path?some-param=some-value&auth_provider_hint=oidc1');
+ });
+ });
});
diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts
index f86ff54963da9..4eeadf23c50b2 100644
--- a/x-pack/plugins/security/server/authentication/authenticator.ts
+++ b/x-pack/plugins/security/server/authentication/authenticator.ts
@@ -11,6 +11,7 @@ import type { IBasePath, IClusterClient, LoggerFactory } from 'src/core/server';
import { KibanaRequest } from '../../../../../src/core/server';
import {
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
LOGOUT_PROVIDER_QUERY_STRING_PARAMETER,
LOGOUT_REASON_QUERY_STRING_PARAMETER,
NEXT_URL_QUERY_STRING_PARAMETER,
@@ -45,6 +46,15 @@ import {
} from './providers';
import { Tokens } from './tokens';
+/**
+ * List of query string parameters used to pass various authentication related metadata that should
+ * be stripped away from URL as soon as they are no longer needed.
+ */
+const AUTH_METADATA_QUERY_STRING_PARAMETERS = [
+ AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
+];
+
/**
* The shape of the login attempt.
*/
@@ -201,6 +211,7 @@ export class Authenticator {
const providerCommonOptions = {
client: this.options.clusterClient,
basePath: this.options.basePath,
+ getRequestOriginalURL: this.getRequestOriginalURL.bind(this),
tokens: new Tokens({
client: this.options.clusterClient.asInternalUser,
logger: this.options.loggers.get('tokens'),
@@ -419,7 +430,9 @@ export class Authenticator {
}
}
- return DeauthenticationResult.notHandled();
+ // If none of the configured providers could perform a logout, we should redirect user to the
+ // default logout location.
+ return DeauthenticationResult.redirectTo(this.getLoggedOutURL(request));
}
/**
@@ -452,6 +465,24 @@ export class Authenticator {
this.options.featureUsageService.recordPreAccessAgreementUsage();
}
+ getRequestOriginalURL(
+ request: KibanaRequest,
+ additionalQueryStringParameters?: Array<[string, string]>
+ ) {
+ const originalURLSearchParams = [
+ ...[...request.url.searchParams.entries()].filter(
+ ([key]) => !AUTH_METADATA_QUERY_STRING_PARAMETERS.includes(key)
+ ),
+ ...(additionalQueryStringParameters ?? []),
+ ];
+
+ return `${this.options.basePath.get(request)}${request.url.pathname}${
+ originalURLSearchParams.length > 0
+ ? `?${new URLSearchParams(originalURLSearchParams).toString()}`
+ : ''
+ }`;
+ }
+
/**
* Initializes HTTP Authentication provider and appends it to the end of the list of enabled
* authentication providers.
@@ -762,9 +793,13 @@ export class Authenticator {
/**
* Creates a logged out URL for the specified request and provider.
* @param request Request that initiated logout.
- * @param providerType Type of the provider that handles logout.
+ * @param providerType Type of the provider that handles logout. If not specified, then the first
+ * provider in the chain (default) is assumed.
*/
- private getLoggedOutURL(request: KibanaRequest, providerType: string) {
+ private getLoggedOutURL(
+ request: KibanaRequest,
+ providerType: string = this.options.config.authc.sortedProviders[0].type
+ ) {
// The app that handles logout needs to know the reason of the logout and the URL we may need to
// redirect user to once they log in again (e.g. when session expires).
const searchParams = new URLSearchParams();
diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts
index 1507cd2d3a50a..805d647757ca5 100644
--- a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts
+++ b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts
@@ -7,6 +7,7 @@
import { httpServerMock } from 'src/core/server/mocks';
+import { ROUTE_TAG_API, ROUTE_TAG_CAN_REDIRECT } from '../routes/tags';
import { canRedirectRequest } from './can_redirect_request';
describe('can_redirect_request', () => {
@@ -24,4 +25,33 @@ describe('can_redirect_request', () => {
expect(canRedirectRequest(request)).toBe(false);
});
+
+ it('returns false for api routes', () => {
+ expect(
+ canRedirectRequest(httpServerMock.createKibanaRequest({ path: '/api/security/some' }))
+ ).toBe(false);
+ });
+
+ it('returns false for internal routes', () => {
+ expect(
+ canRedirectRequest(httpServerMock.createKibanaRequest({ path: '/internal/security/some' }))
+ ).toBe(false);
+ });
+
+ it('returns true for the routes with the `security:canRedirect` tag', () => {
+ for (const request of [
+ httpServerMock.createKibanaRequest({ routeTags: [ROUTE_TAG_CAN_REDIRECT] }),
+ httpServerMock.createKibanaRequest({ routeTags: [ROUTE_TAG_API, ROUTE_TAG_CAN_REDIRECT] }),
+ httpServerMock.createKibanaRequest({
+ path: '/api/security/some',
+ routeTags: [ROUTE_TAG_CAN_REDIRECT],
+ }),
+ httpServerMock.createKibanaRequest({
+ path: '/internal/security/some',
+ routeTags: [ROUTE_TAG_CAN_REDIRECT],
+ }),
+ ]) {
+ expect(canRedirectRequest(request)).toBe(true);
+ }
+ });
});
diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.ts
index 71c6365d9aea4..5a3a09f17eb86 100644
--- a/x-pack/plugins/security/server/authentication/can_redirect_request.ts
+++ b/x-pack/plugins/security/server/authentication/can_redirect_request.ts
@@ -7,7 +7,8 @@
import type { KibanaRequest } from 'src/core/server';
-const ROUTE_TAG_API = 'api';
+import { ROUTE_TAG_API, ROUTE_TAG_CAN_REDIRECT } from '../routes/tags';
+
const KIBANA_XSRF_HEADER = 'kbn-xsrf';
const KIBANA_VERSION_HEADER = 'kbn-version';
@@ -24,9 +25,9 @@ export function canRedirectRequest(request: KibanaRequest) {
const isApiRoute =
route.options.tags.includes(ROUTE_TAG_API) ||
- (route.path.startsWith('/api/') && route.path !== '/api/security/logout') ||
+ route.path.startsWith('/api/') ||
route.path.startsWith('/internal/');
const isAjaxRequest = hasVersionHeader || hasXsrfHeader;
- return !isApiRoute && !isAjaxRequest;
+ return !isAjaxRequest && (!isApiRoute || route.options.tags.includes(ROUTE_TAG_CAN_REDIRECT));
}
diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts
index bb78b6e963763..5d3417ae9db11 100644
--- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts
+++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts
@@ -20,6 +20,7 @@ export function mockAuthenticationProviderOptions(options?: { name: string }) {
client: elasticsearchServiceMock.createClusterClient(),
logger: loggingSystemMock.create().get(),
basePath: httpServiceMock.createBasePath(),
+ getRequestOriginalURL: jest.fn(),
tokens: { refresh: jest.fn(), invalidate: jest.fn() },
name: options?.name ?? 'basic1',
urls: {
diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts
index 18d567a143fee..c7c0edcf1e9e1 100644
--- a/x-pack/plugins/security/server/authentication/providers/base.ts
+++ b/x-pack/plugins/security/server/authentication/providers/base.ts
@@ -27,6 +27,10 @@ import type { Tokens } from '../tokens';
export interface AuthenticationProviderOptions {
name: string;
basePath: HttpServiceSetup['basePath'];
+ getRequestOriginalURL: (
+ request: KibanaRequest,
+ additionalQueryStringParameters?: Array<[string, string]>
+ ) => string;
client: IClusterClient;
logger: Logger;
tokens: PublicMethodsOf;
diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts
index ebeca42682eb9..444a7f3e50a25 100644
--- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts
+++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts
@@ -11,6 +11,10 @@ import Boom from '@hapi/boom';
import type { KibanaRequest } from 'src/core/server';
import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks';
+import {
+ AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
+} from '../../../common/constants';
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import { securityMock } from '../../mocks';
import { AuthenticationResult } from '../authentication_result';
@@ -376,18 +380,78 @@ describe('OIDCAuthenticationProvider', () => {
});
it('redirects non-AJAX request that can not be authenticated to the "capture URL" page.', async () => {
+ mockOptions.getRequestOriginalURL.mockReturnValue(
+ '/mock-server-basepath/s/foo/some-path?auth_provider_hint=oidc'
+ );
const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' });
await expect(provider.authenticate(request, null)).resolves.toEqual(
AuthenticationResult.redirectTo(
- '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path&providerType=oidc&providerName=oidc',
+ '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%3Fauth_provider_hint%3Doidc',
{ state: null }
)
);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledTimes(1);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledWith(request, [
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, 'oidc'],
+ ]);
+
expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled();
});
+ it('initiates OIDC handshake for non-AJAX request that can not be authenticated, but includes URL hash fragment.', async () => {
+ mockOptions.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/s/foo/some-path');
+ mockOptions.client.asInternalUser.transport.request.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ state: 'statevalue',
+ nonce: 'noncevalue',
+ redirect:
+ 'https://op-host/path/login?response_type=code' +
+ '&scope=openid%20profile%20email' +
+ '&client_id=s6BhdRkqt3' +
+ '&state=statevalue' +
+ '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' +
+ '&login_hint=loginhint',
+ },
+ })
+ );
+
+ const request = httpServerMock.createKibanaRequest({
+ path: '/s/foo/some-path',
+ query: { [AUTH_URL_HASH_QUERY_STRING_PARAMETER]: '#some-fragment' },
+ });
+ await expect(provider.authenticate(request)).resolves.toEqual(
+ AuthenticationResult.redirectTo(
+ 'https://op-host/path/login?response_type=code' +
+ '&scope=openid%20profile%20email' +
+ '&client_id=s6BhdRkqt3' +
+ '&state=statevalue' +
+ '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' +
+ '&login_hint=loginhint',
+ {
+ state: {
+ state: 'statevalue',
+ nonce: 'noncevalue',
+ redirectURL: '/mock-server-basepath/s/foo/some-path#some-fragment',
+ realm: 'oidc1',
+ },
+ }
+ )
+ );
+
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledTimes(1);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledWith(request);
+
+ expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1);
+ expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({
+ method: 'POST',
+ path: '/_security/oidc/prepare',
+ body: { realm: 'oidc1' },
+ });
+ });
+
it('succeeds if state contains a valid token.', async () => {
const request = httpServerMock.createKibanaRequest({ headers: {} });
const tokenPair = {
@@ -520,6 +584,9 @@ describe('OIDCAuthenticationProvider', () => {
});
it('redirects non-AJAX requests to the "capture URL" page if refresh token is expired or already refreshed.', async () => {
+ mockOptions.getRequestOriginalURL.mockReturnValue(
+ '/mock-server-basepath/s/foo/some-path?auth_provider_hint=oidc'
+ );
const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path', headers: {} });
const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' };
const authorization = `Bearer ${tokenPair.accessToken}`;
@@ -534,11 +601,16 @@ describe('OIDCAuthenticationProvider', () => {
provider.authenticate(request, { ...tokenPair, realm: 'oidc1' })
).resolves.toEqual(
AuthenticationResult.redirectTo(
- '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path&providerType=oidc&providerName=oidc',
+ '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%3Fauth_provider_hint%3Doidc',
{ state: null }
)
);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledTimes(1);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledWith(request, [
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, 'oidc'],
+ ]);
+
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken);
diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts
index 2afa49fe6e082..83f0ec50abb0d 100644
--- a/x-pack/plugins/security/server/authentication/providers/oidc.ts
+++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts
@@ -10,8 +10,13 @@ import type from 'type-detect';
import type { KibanaRequest } from 'src/core/server';
-import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants';
+import {
+ AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
+ NEXT_URL_QUERY_STRING_PARAMETER,
+} from '../../../common/constants';
import type { AuthenticationInfo } from '../../elasticsearch';
+import { getDetailedErrorMessage } from '../../errors';
import { AuthenticationResult } from '../authentication_result';
import { canRedirectRequest } from '../can_redirect_request';
import { DeauthenticationResult } from '../deauthentication_result';
@@ -201,7 +206,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
// We might already have a state and nonce generated by Elasticsearch (from an unfinished authentication in
// another tab)
return authenticationResult.notHandled() && canStartNewSession(request)
- ? await this.captureRedirectURL(request)
+ ? await this.initiateAuthenticationHandshake(request)
: authenticationResult;
}
@@ -264,7 +269,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
})
).body as any;
} catch (err) {
- this.logger.debug(`Failed to authenticate request via OpenID Connect: ${err.message}`);
+ this.logger.debug(
+ `Failed to authenticate request via OpenID Connect: ${getDetailedErrorMessage(err)}`
+ );
return AuthenticationResult.failed(err);
}
@@ -313,7 +320,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
{ state: { state, nonce, redirectURL, realm: this.realm } }
);
} catch (err) {
- this.logger.debug(`Failed to initiate OpenID Connect authentication: ${err.message}`);
+ this.logger.debug(
+ `Failed to initiate OpenID Connect authentication: ${getDetailedErrorMessage(err)}`
+ );
return AuthenticationResult.failed(err);
}
}
@@ -341,7 +350,9 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Request has been authenticated via state.');
return AuthenticationResult.succeeded(user, { authHeaders });
} catch (err) {
- this.logger.debug(`Failed to authenticate request via state: ${err.message}`);
+ this.logger.debug(
+ `Failed to authenticate request via state: ${getDetailedErrorMessage(err)}`
+ );
return AuthenticationResult.failed(err);
}
}
@@ -379,7 +390,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug(
'Both elasticsearch access and refresh tokens are expired. Re-initiating OpenID Connect authentication.'
);
- return this.captureRedirectURL(request);
+ return this.initiateAuthenticationHandshake(request);
}
return AuthenticationResult.failed(
@@ -440,7 +451,7 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
return DeauthenticationResult.redirectTo(redirect);
}
} catch (err) {
- this.logger.debug(`Failed to deauthenticate user: ${err.message}`);
+ this.logger.debug(`Failed to deauthenticate user: ${getDetailedErrorMessage(err)}`);
return DeauthenticationResult.failed(err);
}
}
@@ -457,22 +468,29 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider {
}
/**
- * Tries to capture full redirect URL (both path and fragment) and initiate OIDC handshake.
+ * Tries to initiate OIDC authentication handshake. If the request already includes user URL hash fragment, we will
+ * initiate handshake right away, otherwise we'll redirect user to a dedicated page where we capture URL hash fragment
+ * first and only then initiate SAML handshake.
* @param request Request instance.
*/
- private captureRedirectURL(request: KibanaRequest) {
- const searchParams = new URLSearchParams([
- [
- NEXT_URL_QUERY_STRING_PARAMETER,
- `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`,
- ],
- ['providerType', this.type],
- ['providerName', this.options.name],
- ]);
+ private initiateAuthenticationHandshake(request: KibanaRequest) {
+ const originalURLHash = request.url.searchParams.get(AUTH_URL_HASH_QUERY_STRING_PARAMETER);
+ if (originalURLHash != null) {
+ return this.initiateOIDCAuthentication(
+ request,
+ { realm: this.realm },
+ `${this.options.getRequestOriginalURL(request)}${originalURLHash}`
+ );
+ }
+
return AuthenticationResult.redirectTo(
`${
this.options.basePath.serverBasePath
- }/internal/security/capture-url?${searchParams.toString()}`,
+ }/internal/security/capture-url?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
+ this.options.getRequestOriginalURL(request, [
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, this.options.name],
+ ])
+ )}`,
// Here we indicate that current session, if any, should be invalidated. It is a no-op for the
// initial handshake, but is essential when both access and refresh tokens are expired.
{ state: null }
diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts
index bd51a0f815329..dfcdb66e61c35 100644
--- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts
+++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts
@@ -10,6 +10,10 @@ import Boom from '@hapi/boom';
import { elasticsearchServiceMock, httpServerMock } from 'src/core/server/mocks';
+import {
+ AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
+} from '../../../common/constants';
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import { securityMock } from '../../mocks';
import { AuthenticationResult } from '../authentication_result';
@@ -848,18 +852,63 @@ describe('SAMLAuthenticationProvider', () => {
});
it('redirects non-AJAX request that can not be authenticated to the "capture URL" page.', async () => {
+ mockOptions.getRequestOriginalURL.mockReturnValue(
+ '/mock-server-basepath/s/foo/some-path?auth_provider_hint=saml'
+ );
const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' });
-
await expect(provider.authenticate(request)).resolves.toEqual(
AuthenticationResult.redirectTo(
- '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path&providerType=saml&providerName=saml',
+ '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%3Fauth_provider_hint%3Dsaml',
{ state: null }
)
);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledTimes(1);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledWith(request, [
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, 'saml'],
+ ]);
+
expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled();
});
+ it('initiates SAML handshake for non-AJAX request that can not be authenticated, but includes URL hash fragment.', async () => {
+ mockOptions.getRequestOriginalURL.mockReturnValue('/mock-server-basepath/s/foo/some-path');
+ mockOptions.client.asInternalUser.transport.request.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ id: 'some-request-id',
+ redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20',
+ },
+ })
+ );
+
+ const request = httpServerMock.createKibanaRequest({
+ path: '/s/foo/some-path',
+ query: { [AUTH_URL_HASH_QUERY_STRING_PARAMETER]: '#some-fragment' },
+ });
+ await expect(provider.authenticate(request)).resolves.toEqual(
+ AuthenticationResult.redirectTo(
+ 'https://idp-host/path/login?SAMLRequest=some%20request%20',
+ {
+ state: {
+ requestId: 'some-request-id',
+ redirectURL: '/mock-server-basepath/s/foo/some-path#some-fragment',
+ realm: 'test-realm',
+ },
+ }
+ )
+ );
+
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledTimes(1);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledWith(request);
+
+ expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({
+ method: 'POST',
+ path: '/_security/saml/prepare',
+ body: { realm: 'test-realm' },
+ });
+ });
+
it('succeeds if state contains a valid token.', async () => {
const request = httpServerMock.createKibanaRequest({ headers: {} });
const state = {
@@ -1024,6 +1073,9 @@ describe('SAMLAuthenticationProvider', () => {
});
it('re-capture URL for non-AJAX requests if refresh token is expired.', async () => {
+ mockOptions.getRequestOriginalURL.mockReturnValue(
+ '/mock-server-basepath/s/foo/some-path?auth_provider_hint=saml'
+ );
const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path', headers: {} });
const state = {
accessToken: 'expired-token',
@@ -1040,11 +1092,16 @@ describe('SAMLAuthenticationProvider', () => {
await expect(provider.authenticate(request, state)).resolves.toEqual(
AuthenticationResult.redirectTo(
- '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path&providerType=saml&providerName=saml',
+ '/mock-server-basepath/internal/security/capture-url?next=%2Fmock-server-basepath%2Fs%2Ffoo%2Fsome-path%3Fauth_provider_hint%3Dsaml',
{ state: null }
)
);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledTimes(1);
+ expect(mockOptions.getRequestOriginalURL).toHaveBeenCalledWith(request, [
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, 'saml'],
+ ]);
+
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken);
diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts
index 7c27e2ebeff10..ea818e5df6e12 100644
--- a/x-pack/plugins/security/server/authentication/providers/saml.ts
+++ b/x-pack/plugins/security/server/authentication/providers/saml.ts
@@ -9,9 +9,14 @@ import Boom from '@hapi/boom';
import type { KibanaRequest } from 'src/core/server';
-import { NEXT_URL_QUERY_STRING_PARAMETER } from '../../../common/constants';
+import {
+ AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
+ AUTH_URL_HASH_QUERY_STRING_PARAMETER,
+ NEXT_URL_QUERY_STRING_PARAMETER,
+} from '../../../common/constants';
import { isInternalURL } from '../../../common/is_internal_url';
import type { AuthenticationInfo } from '../../elasticsearch';
+import { getDetailedErrorMessage } from '../../errors';
import { AuthenticationResult } from '../authentication_result';
import { canRedirectRequest } from '../can_redirect_request';
import { DeauthenticationResult } from '../deauthentication_result';
@@ -185,7 +190,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
} else {
this.logger.debug(
`Failed to perform a login: ${
- authenticationResult.error && authenticationResult.error.message
+ authenticationResult.error && getDetailedErrorMessage(authenticationResult.error)
}`
);
}
@@ -230,7 +235,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
// If we couldn't authenticate by means of all methods above, let's try to capture user URL and
// initiate SAML handshake, otherwise just return authentication result we have.
return authenticationResult.notHandled() && canStartNewSession(request)
- ? this.captureRedirectURL(request)
+ ? this.initiateAuthenticationHandshake(request)
: authenticationResult;
}
@@ -283,7 +288,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
return DeauthenticationResult.redirectTo(redirect);
}
} catch (err) {
- this.logger.debug(`Failed to deauthenticate user: ${err.message}`);
+ this.logger.debug(`Failed to deauthenticate user: ${getDetailedErrorMessage(err)}`);
return DeauthenticationResult.failed(err);
}
}
@@ -362,7 +367,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
})
).body as any;
} catch (err) {
- this.logger.debug(`Failed to log in with SAML response: ${err.message}`);
+ this.logger.debug(`Failed to log in with SAML response: ${getDetailedErrorMessage(err)}`);
// Since we don't know upfront what realm is targeted by the Identity Provider initiated login
// there is a chance that it failed because of realm mismatch and hence we should return
@@ -452,7 +457,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
refreshToken: existingState.refreshToken!,
});
} catch (err) {
- this.logger.debug(`Failed to perform IdP initiated local logout: ${err.message}`);
+ this.logger.debug(
+ `Failed to perform IdP initiated local logout: ${getDetailedErrorMessage(err)}`
+ );
return AuthenticationResult.failed(err);
}
@@ -483,7 +490,9 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug('Request has been authenticated via state.');
return AuthenticationResult.succeeded(user, { authHeaders });
} catch (err) {
- this.logger.debug(`Failed to authenticate request via state: ${err.message}`);
+ this.logger.debug(
+ `Failed to authenticate request via state: ${getDetailedErrorMessage(err)}`
+ );
return AuthenticationResult.failed(err);
}
}
@@ -520,7 +529,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
this.logger.debug(
'Both access and refresh tokens are expired. Capturing redirect URL and re-initiating SAML handshake.'
);
- return this.captureRedirectURL(request);
+ return this.initiateAuthenticationHandshake(request);
}
return AuthenticationResult.failed(
@@ -569,7 +578,7 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
state: { requestId, redirectURL, realm: this.realm },
});
} catch (err) {
- this.logger.debug(`Failed to initiate SAML handshake: ${err.message}`);
+ this.logger.debug(`Failed to initiate SAML handshake: ${getDetailedErrorMessage(err)}`);
return AuthenticationResult.failed(err);
}
}
@@ -629,22 +638,28 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
}
/**
- * Tries to capture full redirect URL (both path and fragment) and initiate SAML handshake.
+ * Tries to initiate SAML authentication handshake. If the request already includes user URL hash fragment, we will
+ * initiate handshake right away, otherwise we'll redirect user to a dedicated page where we capture URL hash fragment
+ * first and only then initiate SAML handshake.
* @param request Request instance.
*/
- private captureRedirectURL(request: KibanaRequest) {
- const searchParams = new URLSearchParams([
- [
- NEXT_URL_QUERY_STRING_PARAMETER,
- `${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`,
- ],
- ['providerType', this.type],
- ['providerName', this.options.name],
- ]);
+ private initiateAuthenticationHandshake(request: KibanaRequest) {
+ const originalURLHash = request.url.searchParams.get(AUTH_URL_HASH_QUERY_STRING_PARAMETER);
+ if (originalURLHash != null) {
+ return this.authenticateViaHandshake(
+ request,
+ `${this.options.getRequestOriginalURL(request)}${originalURLHash}`
+ );
+ }
+
return AuthenticationResult.redirectTo(
`${
this.options.basePath.serverBasePath
- }/internal/security/capture-url?${searchParams.toString()}`,
+ }/internal/security/capture-url?${NEXT_URL_QUERY_STRING_PARAMETER}=${encodeURIComponent(
+ this.options.getRequestOriginalURL(request, [
+ [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, this.options.name],
+ ])
+ )}`,
// Here we indicate that current session, if any, should be invalidated. It is a no-op for the
// initial handshake, but is essential when both access and refresh tokens are expired.
{ state: null }
diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts
index 8f6dd9275e59c..1adbb2dc66533 100644
--- a/x-pack/plugins/security/server/authentication/tokens.ts
+++ b/x-pack/plugins/security/server/authentication/tokens.ts
@@ -8,7 +8,7 @@
import type { ElasticsearchClient, Logger } from 'src/core/server';
import type { AuthenticationInfo } from '../elasticsearch';
-import { getErrorStatusCode } from '../errors';
+import { getDetailedErrorMessage, getErrorStatusCode } from '../errors';
/**
* Represents a pair of access and refresh tokens.
@@ -73,11 +73,11 @@ export class Tokens {
return {
accessToken,
refreshToken,
- // @ts-expect-error @elastic/elasticsearch decalred GetUserAccessTokenResponse.authentication: string
+ // @ts-expect-error @elastic/elasticsearch declared GetUserAccessTokenResponse.authentication: string
authenticationInfo: authenticationInfo as AuthenticationInfo,
};
} catch (err) {
- this.logger.debug(`Failed to refresh access token: ${err.message}`);
+ this.logger.debug(`Failed to refresh access token: ${getDetailedErrorMessage(err)}`);
// There are at least two common cases when refresh token request can fail:
// 1. Refresh token is valid only for 24 hours and if it hasn't been used it expires.
@@ -123,7 +123,7 @@ export class Tokens {
})
).body.invalidated_tokens;
} catch (err) {
- this.logger.debug(`Failed to invalidate refresh token: ${err.message}`);
+ this.logger.debug(`Failed to invalidate refresh token: ${getDetailedErrorMessage(err)}`);
// When using already deleted refresh token, Elasticsearch responds with 404 and a body that
// shows that no tokens were invalidated.
@@ -155,7 +155,7 @@ export class Tokens {
})
).body.invalidated_tokens;
} catch (err) {
- this.logger.debug(`Failed to invalidate access token: ${err.message}`);
+ this.logger.debug(`Failed to invalidate access token: ${getDetailedErrorMessage(err)}`);
// When using already deleted access token, Elasticsearch responds with 404 and a body that
// shows that no tokens were invalidated.
diff --git a/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx b/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx
new file mode 100644
index 0000000000000..5cb6c899d7560
--- /dev/null
+++ b/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+import { coreMock } from '../../../../../src/core/server/mocks';
+import { UnauthenticatedPage } from './unauthenticated_page';
+
+jest.mock('src/core/server/rendering/views/fonts', () => ({
+ Fonts: () => <>MockedFonts>,
+}));
+
+describe('UnauthenticatedPage', () => {
+ it('renders as expected', async () => {
+ const mockCoreSetup = coreMock.createSetup();
+ (mockCoreSetup.http.basePath.prepend as jest.Mock).mockImplementation(
+ (path) => `/mock-basepath${path}`
+ );
+
+ const body = renderToStaticMarkup(
+
+ );
+
+ expect(body).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx b/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx
new file mode 100644
index 0000000000000..48d61a72e085d
--- /dev/null
+++ b/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// @ts-expect-error no definitions in component folder
+import { EuiButton } from '@elastic/eui/lib/components/button';
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import type { IBasePath } from 'src/core/server';
+
+import { PromptPage } from '../prompt_page';
+
+interface Props {
+ originalURL: string;
+ buildNumber: number;
+ basePath: IBasePath;
+}
+
+export function UnauthenticatedPage({ basePath, originalURL, buildNumber }: Props) {
+ return (
+
+
+
+ }
+ actions={[
+
+
+ ,
+ ]}
+ />
+ );
+}
+
+export function renderUnauthenticatedPage(props: Props) {
+ return renderToStaticMarkup( );
+}
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
index 785c57490e8ef..1011d82eb1f73 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ResetSessionPage renders as expected 1`] = `" MockedFonts
You do not have permission to access the requested page
Either go back to the previous page or log in as a different user.
"`;
+exports[`ResetSessionPage renders as expected 1`] = `"Elastic MockedFonts
You do not have permission to access the requested page
Either go back to the previous page or log in as a different user.
"`;
diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx
index db3c84477ffb1..144a8bc5fd0c4 100644
--- a/x-pack/plugins/security/server/authorization/authorization_service.tsx
+++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx
@@ -10,7 +10,6 @@ import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import type { Observable, Subscription } from 'rxjs';
-import * as UiSharedDeps from '@kbn/ui-shared-deps';
import type {
CapabilitiesSetup,
HttpServiceSetup,
@@ -163,25 +162,14 @@ export class AuthorizationService {
http.registerOnPreResponse((request, preResponse, toolkit) => {
if (preResponse.statusCode === 403 && canRedirectRequest(request)) {
- const basePath = http.basePath.get(request);
- const next = `${basePath}${request.url.pathname}${request.url.search}`;
- const regularBundlePath = `${basePath}/${buildNumber}/bundles`;
-
- const logoutUrl = http.basePath.prepend(
- `/api/security/logout?${querystring.stringify({ next })}`
- );
- const styleSheetPaths = [
- `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
- `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`,
- `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
- `${basePath}/ui/legacy_light_theme.css`,
- ];
-
+ const next = `${http.basePath.get(request)}${request.url.pathname}${request.url.search}`;
const body = renderToStaticMarkup(
);
diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.test.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.test.tsx
index e76c8ff138fcb..d5e27c9d39ffd 100644
--- a/x-pack/plugins/security/server/authorization/reset_session_page.test.tsx
+++ b/x-pack/plugins/security/server/authorization/reset_session_page.test.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
+import { coreMock } from '../../../../../src/core/server/mocks';
import { ResetSessionPage } from './reset_session_page';
jest.mock('src/core/server/rendering/views/fonts', () => ({
@@ -16,11 +17,16 @@ jest.mock('src/core/server/rendering/views/fonts', () => ({
describe('ResetSessionPage', () => {
it('renders as expected', async () => {
+ const mockCoreSetup = coreMock.createSetup();
+ (mockCoreSetup.http.basePath.prepend as jest.Mock).mockImplementation(
+ (path) => `/mock-basepath${path}`
+ );
+
const body = renderToStaticMarkup(
);
diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.tsx
index c2d43cd3dd030..4e2e6f4631287 100644
--- a/x-pack/plugins/security/server/authorization/reset_session_page.tsx
+++ b/x-pack/plugins/security/server/authorization/reset_session_page.tsx
@@ -7,101 +7,53 @@
// @ts-expect-error no definitions in component folder
import { EuiButton, EuiButtonEmpty } from '@elastic/eui/lib/components/button';
-// @ts-expect-error no definitions in component folder
-import { EuiEmptyPrompt } from '@elastic/eui/lib/components/empty_prompt';
-// @ts-expect-error no definitions in component folder
-import { icon as EuiIconAlert } from '@elastic/eui/lib/components/icon/assets/alert';
-// @ts-expect-error no definitions in component folder
-import { appendIconComponentCache } from '@elastic/eui/lib/components/icon/icon';
-// @ts-expect-error no definitions in component folder
-import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui/lib/components/page';
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
-
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { Fonts } from '../../../../../src/core/server/rendering/views/fonts';
+import { FormattedMessage } from '@kbn/i18n/react';
+import type { IBasePath } from 'src/core/server';
-// Preload the alert icon used by `EuiEmptyPrompt` to ensure that it's loaded
-// in advance the first time this page is rendered server-side. If not, the
-// icon svg wouldn't contain any paths the first time the page was rendered.
-appendIconComponentCache({
- alert: EuiIconAlert,
-});
+import { PromptPage } from '../prompt_page';
export function ResetSessionPage({
logoutUrl,
- styleSheetPaths,
+ buildNumber,
basePath,
}: {
logoutUrl: string;
- styleSheetPaths: string[];
- basePath: string;
+ buildNumber: number;
+ basePath: IBasePath;
}) {
- const uiPublicUrl = `${basePath}/ui`;
return (
-
-
- {styleSheetPaths.map((path) => (
-
- ))}
-
- {/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- body={
-
-
-
- }
- actions={[
-
-
- ,
-
-
- ,
- ]}
- />
-
-
-
-
-
-
+
+
+
+ }
+ actions={[
+
+
+ ,
+
+
+ ,
+ ]}
+ />
);
}
diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts
index b66ed6e9eb7ca..087cf8f4f8ee8 100644
--- a/x-pack/plugins/security/server/index.ts
+++ b/x-pack/plugins/security/server/index.ts
@@ -30,6 +30,7 @@ export type { CheckPrivilegesPayload } from './authorization';
export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit';
export type { SecurityPluginSetup, SecurityPluginStart };
export type { AuthenticatedUser } from '../common/model';
+export { ROUTE_TAG_CAN_REDIRECT } from './routes/tags';
export const config: PluginConfigDescriptor> = {
schema: ConfigSchema,
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 586707dd8c9aa..57be308525fdd 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -246,7 +246,12 @@ export class SecurityPlugin
this.elasticsearchService.setup({ license, status: core.status });
this.featureUsageService.setup({ featureUsage: licensing.featureUsage });
this.sessionManagementService.setup({ config, http: core.http, taskManager });
- this.authenticationService.setup({ http: core.http, license });
+ this.authenticationService.setup({
+ http: core.http,
+ config,
+ license,
+ buildNumber: this.initializerContext.env.packageInfo.buildNum,
+ });
registerSecurityUsageCollector({ usageCollection, config, license });
diff --git a/x-pack/plugins/security/server/prompt_page.test.tsx b/x-pack/plugins/security/server/prompt_page.test.tsx
new file mode 100644
index 0000000000000..01c4488576f57
--- /dev/null
+++ b/x-pack/plugins/security/server/prompt_page.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+import { coreMock } from '../../../../src/core/server/mocks';
+import { PromptPage } from './prompt_page';
+
+jest.mock('src/core/server/rendering/views/fonts', () => ({
+ Fonts: () => <>MockedFonts>,
+}));
+
+describe('PromptPage', () => {
+ it('renders as expected without additional scripts', async () => {
+ const mockCoreSetup = coreMock.createSetup();
+ (mockCoreSetup.http.basePath.prepend as jest.Mock).mockImplementation(
+ (path) => `/mock-basepath${path}`
+ );
+
+ const body = renderToStaticMarkup(
+ Some Body}
+ actions={[Action#1 , Action#2 ]}
+ />
+ );
+
+ expect(body).toMatchSnapshot();
+ });
+
+ it('renders as expected with additional scripts', async () => {
+ const mockCoreSetup = coreMock.createSetup();
+ (mockCoreSetup.http.basePath.prepend as jest.Mock).mockImplementation(
+ (path) => `/mock-basepath${path}`
+ );
+
+ const body = renderToStaticMarkup(
+ Some Body}
+ actions={[Action#1 , Action#2 ]}
+ />
+ );
+
+ expect(body).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/security/server/prompt_page.tsx b/x-pack/plugins/security/server/prompt_page.tsx
new file mode 100644
index 0000000000000..338d39b29e534
--- /dev/null
+++ b/x-pack/plugins/security/server/prompt_page.tsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+// @ts-expect-error no definitions in component folder
+import { EuiEmptyPrompt } from '@elastic/eui/lib/components/empty_prompt';
+// @ts-expect-error no definitions in component folder
+import { icon as EuiIconAlert } from '@elastic/eui/lib/components/icon/assets/alert';
+// @ts-expect-error no definitions in component folder
+import { appendIconComponentCache } from '@elastic/eui/lib/components/icon/icon';
+// @ts-expect-error no definitions in component folder
+import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui/lib/components/page';
+import type { ReactNode } from 'react';
+import React from 'react';
+
+import { i18n } from '@kbn/i18n';
+import { I18nProvider } from '@kbn/i18n/react';
+import * as UiSharedDeps from '@kbn/ui-shared-deps';
+import type { IBasePath } from 'src/core/server';
+
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { Fonts } from '../../../../src/core/server/rendering/views/fonts';
+
+// Preload the alert icon used by `EuiEmptyPrompt` to ensure that it's loaded
+// in advance the first time this page is rendered server-side. If not, the
+// icon svg wouldn't contain any paths the first time the page was rendered.
+appendIconComponentCache({
+ alert: EuiIconAlert,
+});
+
+interface Props {
+ buildNumber: number;
+ basePath: IBasePath;
+ scriptPaths?: string[];
+ title: ReactNode;
+ body: ReactNode;
+ actions: ReactNode;
+}
+
+export function PromptPage({
+ basePath,
+ buildNumber,
+ scriptPaths = [],
+ title,
+ body,
+ actions,
+}: Props) {
+ const uiPublicURL = `${basePath.serverBasePath}/ui`;
+ const regularBundlePath = `${basePath.serverBasePath}/${buildNumber}/bundles`;
+ const styleSheetPaths = [
+ `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
+ `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`,
+ `${basePath.serverBasePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
+ `${basePath.serverBasePath}/ui/legacy_light_theme.css`,
+ ];
+
+ return (
+
+
+ Elastic
+ {styleSheetPaths.map((path) => (
+
+ ))}
+
+ {/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */}
+
+
+ {scriptPaths.map((path) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ {title}}
+ body={body}
+ actions={actions}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts
index 6b3ce15669c27..1e0b6784a45c8 100644
--- a/x-pack/plugins/security/server/routes/authentication/common.test.ts
+++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts
@@ -23,6 +23,7 @@ import {
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types';
import { routeDefinitionParamsMock } from '../index.mock';
+import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
import { defineCommonRoutes } from './common';
describe('Common authentication routes', () => {
@@ -64,7 +65,10 @@ describe('Common authentication routes', () => {
});
it('correctly defines route.', async () => {
- expect(routeConfig.options).toEqual({ authRequired: false });
+ expect(routeConfig.options).toEqual({
+ authRequired: false,
+ tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
+ });
expect(routeConfig.validate).toEqual({
body: undefined,
query: expect.any(Type),
diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts
index 28c344785da4b..b16aac9743676 100644
--- a/x-pack/plugins/security/server/routes/authentication/common.ts
+++ b/x-pack/plugins/security/server/routes/authentication/common.ts
@@ -21,6 +21,7 @@ import {
} from '../../authentication';
import { wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';
+import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
/**
* Defines routes that are common to various authentication mechanisms.
@@ -40,7 +41,7 @@ export function defineCommonRoutes({
// Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any
// set of query string parameters (e.g. SAML/OIDC logout request/response parameters).
validate: { query: schema.object({}, { unknowns: 'allow' }) },
- options: { authRequired: false },
+ options: { authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW] },
},
async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts
index 854b6721278ff..0216001449d36 100644
--- a/x-pack/plugins/security/server/routes/authentication/oidc.ts
+++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts
@@ -10,11 +10,11 @@ import { i18n } from '@kbn/i18n';
import type { KibanaRequest, KibanaResponseFactory } from 'src/core/server';
import type { RouteDefinitionParams } from '../';
-import { OIDCLogin } from '../../authentication';
+import { OIDCAuthenticationProvider, OIDCLogin } from '../../authentication';
import type { ProviderLoginAttempt } from '../../authentication/providers/oidc';
-import { OIDCAuthenticationProvider } from '../../authentication/providers/oidc';
import { wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';
+import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
/**
* Defines routes required for SAML authentication.
@@ -106,7 +106,7 @@ export function defineOIDCRoutes({
{ unknowns: 'allow' }
),
},
- options: { authRequired: false },
+ options: { authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW] },
},
createLicensedRouteHandler(async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
@@ -183,7 +183,11 @@ export function defineOIDCRoutes({
{ unknowns: 'allow' }
),
},
- options: { authRequired: false, xsrfRequired: false },
+ options: {
+ authRequired: false,
+ xsrfRequired: false,
+ tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
+ },
},
createLicensedRouteHandler(async (context, request, response) => {
const serverBasePath = basePath.serverBasePath;
@@ -222,7 +226,10 @@ export function defineOIDCRoutes({
{ unknowns: 'allow' }
),
},
- options: { authRequired: false },
+ options: {
+ authRequired: false,
+ tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
+ },
},
createLicensedRouteHandler(async (context, request, response) => {
return performOIDCLogin(request, response, {
diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts
index 35fdcff295c1e..3b2497ed9f30b 100644
--- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts
+++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts
@@ -16,6 +16,7 @@ import { AuthenticationResult, SAMLLogin } from '../../authentication';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
import type { SecurityRouter } from '../../types';
import { routeDefinitionParamsMock } from '../index.mock';
+import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
import { defineSAMLRoutes } from './saml';
describe('SAML authentication routes', () => {
@@ -43,7 +44,11 @@ describe('SAML authentication routes', () => {
});
it('correctly defines route.', () => {
- expect(routeConfig.options).toEqual({ authRequired: false, xsrfRequired: false });
+ expect(routeConfig.options).toEqual({
+ authRequired: false,
+ xsrfRequired: false,
+ tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
+ });
expect(routeConfig.validate).toEqual({
body: expect.any(Type),
query: undefined,
diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts
index 5dac46354025a..8a343c2413779 100644
--- a/x-pack/plugins/security/server/routes/authentication/saml.ts
+++ b/x-pack/plugins/security/server/routes/authentication/saml.ts
@@ -8,17 +8,13 @@
import { schema } from '@kbn/config-schema';
import type { RouteDefinitionParams } from '../';
-import { SAMLLogin } from '../../authentication';
-import { SAMLAuthenticationProvider } from '../../authentication/providers';
+import { SAMLAuthenticationProvider, SAMLLogin } from '../../authentication';
+import { ROUTE_TAG_AUTH_FLOW, ROUTE_TAG_CAN_REDIRECT } from '../tags';
/**
* Defines routes required for SAML authentication.
*/
-export function defineSAMLRoutes({
- router,
- logger,
- getAuthenticationService,
-}: RouteDefinitionParams) {
+export function defineSAMLRoutes({ router, getAuthenticationService }: RouteDefinitionParams) {
router.post(
{
path: '/api/security/saml/callback',
@@ -28,7 +24,11 @@ export function defineSAMLRoutes({
{ unknowns: 'ignore' }
),
},
- options: { authRequired: false, xsrfRequired: false },
+ options: {
+ authRequired: false,
+ xsrfRequired: false,
+ tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW],
+ },
},
async (context, request, response) => {
// When authenticating using SAML we _expect_ to redirect to the Kibana target location.
diff --git a/x-pack/plugins/security/server/routes/tags.ts b/x-pack/plugins/security/server/routes/tags.ts
new file mode 100644
index 0000000000000..090c04d29757f
--- /dev/null
+++ b/x-pack/plugins/security/server/routes/tags.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/**
+ * If, for whatever reason, the API route path doesn't follow the API naming convention and doesn't
+ * start with `/api` or `/internal` prefix, it should be marked with this tag explicitly to let
+ * Security know that it should be handled as any other API route.
+ */
+export const ROUTE_TAG_API = 'api';
+
+/**
+ * If the route is marked with this tag Security can safely assume that the calling party that sends
+ * request to this route can handle redirect responses. It's particularly important if we want the
+ * specific route to be able to initiate or participate in the authentication handshake that may
+ * involve redirects and will eventually redirect authenticated user to this route.
+ */
+export const ROUTE_TAG_CAN_REDIRECT = 'security:canRedirect';
+
+/**
+ * The routes that are involved into authentication flows, especially if they are used by the 3rd
+ * parties, require special handling.
+ */
+export const ROUTE_TAG_AUTH_FLOW = 'security:authFlow';
diff --git a/x-pack/plugins/security/server/routes/views/capture_url.test.ts b/x-pack/plugins/security/server/routes/views/capture_url.test.ts
index 0393a69276ff3..6c4de7a719fe1 100644
--- a/x-pack/plugins/security/server/routes/views/capture_url.test.ts
+++ b/x-pack/plugins/security/server/routes/views/capture_url.test.ts
@@ -43,46 +43,10 @@ describe('Capture URL view routes', () => {
});
const queryValidator = (routeConfig.validate as any).query as Type;
- expect(
- queryValidator.validate({ providerType: 'basic', providerName: 'basic1', next: '/some-url' })
- ).toEqual({ providerType: 'basic', providerName: 'basic1', next: '/some-url' });
-
- expect(queryValidator.validate({ providerType: 'basic', providerName: 'basic1' })).toEqual({
- providerType: 'basic',
- providerName: 'basic1',
+ expect(queryValidator.validate({})).toEqual({});
+ expect(queryValidator.validate({ next: '/some-url', something: 'something' })).toEqual({
+ next: '/some-url',
});
-
- expect(() => queryValidator.validate({ providerType: '' })).toThrowErrorMatchingInlineSnapshot(
- `"[providerType]: value has length [0] but it must have a minimum length of [1]."`
- );
-
- expect(() =>
- queryValidator.validate({ providerType: 'basic' })
- ).toThrowErrorMatchingInlineSnapshot(
- `"[providerName]: expected value of type [string] but got [undefined]"`
- );
-
- expect(() => queryValidator.validate({ providerName: '' })).toThrowErrorMatchingInlineSnapshot(
- `"[providerType]: expected value of type [string] but got [undefined]"`
- );
-
- expect(() =>
- queryValidator.validate({ providerName: 'basic1' })
- ).toThrowErrorMatchingInlineSnapshot(
- `"[providerType]: expected value of type [string] but got [undefined]"`
- );
-
- expect(() =>
- queryValidator.validate({ providerType: 'basic', providerName: '' })
- ).toThrowErrorMatchingInlineSnapshot(
- `"[providerName]: value has length [0] but it must have a minimum length of [1]."`
- );
-
- expect(() =>
- queryValidator.validate({ providerType: '', providerName: 'basic1' })
- ).toThrowErrorMatchingInlineSnapshot(
- `"[providerType]: value has length [0] but it must have a minimum length of [1]."`
- );
});
it('renders view.', async () => {
diff --git a/x-pack/plugins/security/server/routes/views/capture_url.ts b/x-pack/plugins/security/server/routes/views/capture_url.ts
index 1ea1c8ad620e4..d345639b58cb2 100644
--- a/x-pack/plugins/security/server/routes/views/capture_url.ts
+++ b/x-pack/plugins/security/server/routes/views/capture_url.ts
@@ -17,11 +17,7 @@ export function defineCaptureURLRoutes({ httpResources }: RouteDefinitionParams)
{
path: '/internal/security/capture-url',
validate: {
- query: schema.object({
- providerType: schema.string({ minLength: 1 }),
- providerName: schema.string({ minLength: 1 }),
- next: schema.maybe(schema.string()),
- }),
+ query: schema.object({ next: schema.maybe(schema.string()) }, { unknowns: 'ignore' }),
},
options: { authRequired: false },
},
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 2d900f9e5dc85..f20bc0f72ccde 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -17161,7 +17161,6 @@
"xpack.security.loggedOut.login": "ログイン",
"xpack.security.loggedOut.title": "ログアウト完了",
"xpack.security.loggedOutAppTitle": "ログアウト",
- "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワードです。再試行してください。",
"xpack.security.login.basicLoginForm.logInButtonLabel": "ログイン",
"xpack.security.login.basicLoginForm.passwordFormRowLabel": "パスワード",
"xpack.security.login.basicLoginForm.unknownErrorMessage": "おっと!エラー。再試行してください。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 69d84e7a0f56b..3b32e1c184f26 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -17400,7 +17400,6 @@
"xpack.security.loggedOut.login": "登录",
"xpack.security.loggedOut.title": "已成功退出",
"xpack.security.loggedOutAppTitle": "已注销",
- "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。",
"xpack.security.login.basicLoginForm.logInButtonLabel": "登录",
"xpack.security.login.basicLoginForm.passwordFormRowLabel": "密码",
"xpack.security.login.basicLoginForm.unknownErrorMessage": "糟糕!错误。请重试。",
diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js
index 04aa451b6491a..e42ba6cb8a055 100644
--- a/x-pack/test/api_integration/apis/security/basic_login.js
+++ b/x-pack/test/api_integration/apis/security/basic_login.js
@@ -303,11 +303,11 @@ export default function ({ getService }) {
expect(loginViewResponse.headers.location).to.be('/');
});
- it('should redirect to home page if cookie is not provided', async () => {
+ it('should redirect to login page if cookie is not provided', async () => {
const logoutResponse = await supertest.get('/api/security/logout').expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
- expect(logoutResponse.headers.location).to.be('/');
+ expect(logoutResponse.headers.location).to.be('/login?msg=LOGGED_OUT');
});
});
});
diff --git a/x-pack/test/functional/apps/security/security.ts b/x-pack/test/functional/apps/security/security.ts
index d9a36233428c5..70db20d6d0c48 100644
--- a/x-pack/test/functional/apps/security/security.ts
+++ b/x-pack/test/functional/apps/security/security.ts
@@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expectSuccess: false,
});
const errorMessage = await PageObjects.security.loginPage.getErrorMessage();
- expect(errorMessage).to.be('Invalid username or password. Please try again.');
+ expect(errorMessage).to.be('Username or password is incorrect. Please try again.');
});
it('displays message acknowledging logout', async () => {
diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts
index 3d1a05583e904..61c8c55c86764 100644
--- a/x-pack/test/security_api_integration/tests/anonymous/login.ts
+++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts
@@ -112,11 +112,16 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should fail if `Authorization` header is present, but not valid', async () => {
- const response = await supertest
+ const unauthenticatedResponse = await supertest
.get('/security/account')
.set('Authorization', 'Basic wow')
.expect(401);
- expect(response.headers['set-cookie']).to.be(undefined);
+
+ expect(unauthenticatedResponse.headers['set-cookie']).to.be(undefined);
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
});
@@ -156,9 +161,14 @@ export default function ({ getService }: FtrProviderContext) {
const apiResponse = await supertest
.get('/internal/security/me')
.set('kbn-xsrf', 'xxx')
- .set('Authorization', 'Basic a3JiNTprcmI1')
+ .set('Authorization', 'Basic ZHVtbXlfaGFja2VyOnBhc3M=')
.set('Cookie', sessionCookie.cookieString())
- .expect(401);
+ .expect(401, {
+ statusCode: 401,
+ error: 'Unauthorized',
+ message:
+ '[security_exception]: unable to authenticate user [dummy_hacker] for REST request [/_security/_authenticate]',
+ });
expect(apiResponse.headers['set-cookie']).to.be(undefined);
});
@@ -203,7 +213,7 @@ export default function ({ getService }: FtrProviderContext) {
const logoutResponse = await supertest.get('/api/security/logout').expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
- expect(logoutResponse.headers.location).to.be('/');
+ expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
});
});
});
diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts
index 3deb1408dc5c9..c0681b5adcac8 100644
--- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts
+++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts
@@ -53,7 +53,10 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should reject API requests if client is not authenticated', async () => {
- await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401);
+ await supertest
+ .get('/internal/security/me')
+ .set('kbn-xsrf', 'xxx')
+ .expect(401, { statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' });
});
it('does not prevent basic login', async () => {
@@ -92,6 +95,13 @@ export default function ({ getService }: FtrProviderContext) {
expect(spnegoResponse.headers['set-cookie']).to.be(undefined);
expect(spnegoResponse.headers['www-authenticate']).to.be('Negotiate');
+
+ // If browser and Kibana can successfully negotiate this HTML won't rendered, but if not
+ // users will see a proper `Unauthenticated` page.
+ expect(spnegoResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(spnegoResponse.text).to.contain('We couldn't log you in');
});
it('AJAX requests should not initiate SPNEGO', async () => {
@@ -285,7 +295,7 @@ export default function ({ getService }: FtrProviderContext) {
const logoutResponse = await supertest.get('/api/security/logout').expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
- expect(logoutResponse.headers.location).to.be('/');
+ expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
});
});
diff --git a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts
index 0d894ce00d11f..69b3542b74bfe 100644
--- a/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts
+++ b/x-pack/test/security_api_integration/tests/login_selector/basic_functionality.ts
@@ -394,7 +394,7 @@ export default function ({ getService }: FtrProviderContext) {
)!;
// And now try to login with `saml2`.
- await supertest
+ const unauthenticatedResponse = await supertest
.post('/api/security/saml/callback')
.ca(CA_CERT)
.set('Cookie', saml1HandshakeCookie.cookieString())
@@ -402,6 +402,30 @@ export default function ({ getService }: FtrProviderContext) {
SAMLResponse: await createSAMLResponse({ issuer: `http://www.elastic.co/saml2` }),
})
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.headers.refresh).to.be(
+ `0;url=/logout?msg=UNAUTHENTICATED&next=%2F`
+ );
+ });
+
+ it('should fail if SAML response is not valid', async () => {
+ const unauthenticatedResponse = await supertest
+ .post('/api/security/saml/callback')
+ .ca(CA_CERT)
+ .send({
+ SAMLResponse: await createSAMLResponse({ inResponseTo: 'some-invalid-request-id' }),
+ })
+ .expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.headers.refresh).to.be(
+ `0;url=/login?msg=UNAUTHENTICATED&next=%2F`
+ );
});
it('should be able to log in via SP initiated login with any configured realm', async () => {
@@ -654,6 +678,41 @@ export default function ({ getService }: FtrProviderContext) {
);
});
+ it('should fail IdP initiated login if state is not matching', async () => {
+ const handshakeResponse = await supertest
+ .get('/api/security/oidc/initiate_login?iss=https://test-op.elastic.co')
+ .ca(CA_CERT)
+ .expect(302);
+ const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
+
+ const unauthenticatedResponse = await supertest
+ .get('/api/security/oidc/callback?code=code2&state=someothervalue')
+ .ca(CA_CERT)
+ .set('Cookie', handshakeCookie.cookieString())
+ .expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.headers.refresh).to.be(
+ `0;url=/logout?msg=UNAUTHENTICATED&next=%2F`
+ );
+ });
+
+ it('should fail IdP initiated login if issuer is not known', async () => {
+ const unauthenticatedResponse = await supertest
+ .get('/api/security/oidc/initiate_login?iss=https://dummy.hacker.co')
+ .ca(CA_CERT)
+ .expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.headers.refresh).to.be(
+ `0;url=/login?msg=UNAUTHENTICATED&next=%2F`
+ );
+ });
+
it('should be able to log in via SP initiated login', async () => {
const handshakeResponse = await supertest
.post('/internal/security/login')
diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts
index c13ce902ad658..940120988b747 100644
--- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts
+++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts
@@ -19,7 +19,10 @@ export default function ({ getService }: FtrProviderContext) {
describe('OpenID Connect authentication', () => {
it('should reject API requests if client is not authenticated', async () => {
- await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401);
+ await supertest
+ .get('/internal/security/me')
+ .set('kbn-xsrf', 'xxx')
+ .expect(401, { statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' });
});
it('does not prevent basic login', async () => {
@@ -57,21 +60,16 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeResponse.headers['set-cookie']).to.be(undefined);
expect(handshakeResponse.headers.location).to.be(
- '/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc'
+ '/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2Bthree%26auth_provider_hint%3Doidc'
);
});
it('should properly set cookie, return all parameters and redirect user', async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const cookies = handshakeResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
@@ -82,7 +80,10 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeCookie.path).to.be('/');
expect(handshakeCookie.httpOnly).to.be(true);
- const redirectURL = url.parse(handshakeResponse.body.location, true /* parseQueryString */);
+ const redirectURL = url.parse(
+ handshakeResponse.headers.location,
+ true /* parseQueryString */
+ );
expect(
redirectURL.href!.startsWith(`https://test-op.elastic.co/oauth2/v1/authorize`)
).to.be(true);
@@ -126,15 +127,10 @@ export default function ({ getService }: FtrProviderContext) {
it('should not allow access to the API with the handshake cookie', async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
await supertest
@@ -160,18 +156,13 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- stateAndNonce = getStateAndNonce(handshakeResponse.body.location);
+ stateAndNonce = getStateAndNonce(handshakeResponse.headers.location);
// Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens
await supertest
.post('/api/oidc_provider/setup')
@@ -181,30 +172,37 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should fail if OpenID Connect response is not complemented with handshake cookie', async () => {
- await supertest
+ const unauthenticatedResponse = await supertest
.get(`/api/security/oidc/callback?code=thisisthecode&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
it('should fail if state is not matching', async () => {
- await supertest
+ const unauthenticatedResponse = await supertest
.get(`/api/security/oidc/callback?code=thisisthecode&state=someothervalue`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
it('should succeed if both the OpenID Connect response and the cookie are provided', async () => {
const oidcAuthenticationResponse = await supertest
.get(`/api/security/oidc/callback?code=code1&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(302);
// User should be redirected to the URL that initiated handshake.
expect(oidcAuthenticationResponse.headers.location).to.be(
- '/abc/xyz/handshake?one=two%20three#/workpad'
+ '/abc/xyz/handshake?one=two+three#/workpad'
);
const cookies = oidcAuthenticationResponse.headers['set-cookie'];
@@ -258,7 +256,6 @@ export default function ({ getService }: FtrProviderContext) {
const oidcAuthenticationResponse = await supertest
.get(`/api/security/oidc/callback?code=code2&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(302);
const cookies = oidcAuthenticationResponse.headers['set-cookie'];
@@ -301,18 +298,13 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
sessionCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- stateAndNonce = getStateAndNonce(handshakeResponse.body.location);
+ stateAndNonce = getStateAndNonce(handshakeResponse.headers.location);
// Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens
await supertest
.post('/api/oidc_provider/setup')
@@ -322,7 +314,6 @@ export default function ({ getService }: FtrProviderContext) {
const oidcAuthenticationResponse = await supertest
.get(`/api/security/oidc/callback?code=code1&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', sessionCookie.cookieString())
.expect(302);
@@ -383,18 +374,13 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const stateAndNonce = getStateAndNonce(handshakeResponse.body.location);
+ const stateAndNonce = getStateAndNonce(handshakeResponse.headers.location);
// Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens
await supertest
.post('/api/oidc_provider/setup')
@@ -404,7 +390,6 @@ export default function ({ getService }: FtrProviderContext) {
const oidcAuthenticationResponse = await supertest
.get(`/api/security/oidc/callback?code=code1&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(302);
@@ -418,7 +403,7 @@ export default function ({ getService }: FtrProviderContext) {
const logoutResponse = await supertest.get('/api/security/logout').expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
- expect(logoutResponse.headers.location).to.be('/');
+ expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
});
it('should redirect to the OPs endsession endpoint to complete logout', async () => {
@@ -472,18 +457,13 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const stateAndNonce = getStateAndNonce(handshakeResponse.body.location);
+ const stateAndNonce = getStateAndNonce(handshakeResponse.headers.location);
// Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens
await supertest
.post('/api/oidc_provider/setup')
@@ -493,7 +473,6 @@ export default function ({ getService }: FtrProviderContext) {
const oidcAuthenticationResponse = await supertest
.get(`/api/security/oidc/callback?code=code1&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(302);
@@ -569,18 +548,13 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const stateAndNonce = getStateAndNonce(handshakeResponse.body.location);
+ const stateAndNonce = getStateAndNonce(handshakeResponse.headers.location);
// Set the nonce in our mock OIDC Provider so that it can generate the ID Tokens
await supertest
.post('/api/oidc_provider/setup')
@@ -590,7 +564,6 @@ export default function ({ getService }: FtrProviderContext) {
const oidcAuthenticationResponse = await supertest
.get(`/api/security/oidc/callback?code=code1&state=${stateAndNonce.state}`)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(302);
@@ -612,16 +585,11 @@ export default function ({ getService }: FtrProviderContext) {
expect(esResponse.body).to.have.property('deleted').greaterThan(0);
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
.set('Cookie', sessionCookie.cookieString())
- .send({
- providerType: 'oidc',
- providerName: 'oidc',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=oidc&providerName=oidc#/workpad',
- })
- .expect(200);
+ .expect(302);
const cookies = handshakeResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
@@ -632,7 +600,10 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeCookie.path).to.be('/');
expect(handshakeCookie.httpOnly).to.be(true);
- const redirectURL = url.parse(handshakeResponse.body.location, true /* parseQueryString */);
+ const redirectURL = url.parse(
+ handshakeResponse.headers.location,
+ true /* parseQueryString */
+ );
expect(
redirectURL.href!.startsWith(`https://test-op.elastic.co/oauth2/v1/authorize`)
).to.be(true);
diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts
index 8f5eb1edba391..b3a04747125e2 100644
--- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts
+++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/oidc_auth.ts
@@ -83,32 +83,39 @@ export default function ({ getService }: FtrProviderContext) {
const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce);
const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`;
- await supertest
+ const unauthenticatedResponse = await supertest
.get(
`/api/security/oidc/callback?authenticationResponseURI=${encodeURIComponent(
authenticationResponse
)}`
)
- .set('kbn-xsrf', 'xxx')
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
it('should fail if state is not matching', async () => {
const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce);
const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=$someothervalue&token_type=bearer&access_token=${accessToken}`;
- await supertest
+ const unauthenticatedResponse = await supertest
.get(
`/api/security/oidc/callback?authenticationResponseURI=${encodeURIComponent(
authenticationResponse
)}`
)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
- // FLAKY: https://github.com/elastic/kibana/issues/43938
it('should succeed if both the OpenID Connect response and the cookie are provided', async () => {
const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce);
const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`;
@@ -119,7 +126,6 @@ export default function ({ getService }: FtrProviderContext) {
authenticationResponse
)}`
)
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.expect(302);
diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts
index 6eca9c354e248..dc2c66721f42a 100644
--- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts
+++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts
@@ -62,7 +62,21 @@ export default function ({ getService }: FtrProviderContext) {
.ca(CA_CERT)
.pfx(UNTRUSTED_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
+ .expect(401, { statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' });
+ });
+
+ it('should fail and redirect if untrusted certificate is used', async () => {
+ // Unlike the call to '/internal/security/me' above, this route can be redirected (see pre-response in `AuthenticationService`).
+ const unauthenticatedResponse = await supertest
+ .get('/security/account')
+ .ca(CA_CERT)
+ .pfx(UNTRUSTED_CLIENT_CERT)
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
it('does not prevent basic login', async () => {
@@ -319,7 +333,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
- expect(logoutResponse.headers.location).to.be('/');
+ expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
});
});
diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts
index 0a76628418c5e..d5fb1e79f80dc 100644
--- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts
+++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts
@@ -74,7 +74,10 @@ export default function ({ getService }: FtrProviderContext) {
describe('SAML authentication', () => {
it('should reject API requests if client is not authenticated', async () => {
- await supertest.get('/internal/security/me').set('kbn-xsrf', 'xxx').expect(401);
+ await supertest
+ .get('/internal/security/me')
+ .set('kbn-xsrf', 'xxx')
+ .expect(401, { statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' });
});
it('does not prevent basic login', async () => {
@@ -112,20 +115,16 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeResponse.headers['set-cookie']).to.be(undefined);
expect(handshakeResponse.headers.location).to.be(
- '/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml'
+ '/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2Bthree%26auth_provider_hint%3Dsaml'
);
});
it('should properly set cookie and redirect user to IdP', async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL: `https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad`,
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const cookies = handshakeResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
@@ -136,21 +135,20 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeCookie.path).to.be('/');
expect(handshakeCookie.httpOnly).to.be(true);
- const redirectURL = url.parse(handshakeResponse.body.location, true /* parseQueryString */);
+ const redirectURL = url.parse(
+ handshakeResponse.headers.location,
+ true /* parseQueryString */
+ );
expect(redirectURL.href!.startsWith(`https://elastic.co/sso/saml`)).to.be(true);
expect(redirectURL.query.SAMLRequest).to.not.be.empty();
});
it('should not allow access to the API with the handshake cookie', async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL: `https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad`,
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
await supertest
@@ -176,39 +174,37 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- samlRequestId = await getSAMLRequestId(handshakeResponse.body.location);
+ samlRequestId = await getSAMLRequestId(handshakeResponse.headers.location);
});
it('should fail if SAML response is not complemented with handshake cookie', async () => {
- await supertest
+ const unauthenticatedResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.send({ SAMLResponse: await createSAMLResponse({ inResponseTo: samlRequestId }) })
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
it('should succeed if both SAML response and handshake cookie are provided', async () => {
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.send({ SAMLResponse: await createSAMLResponse({ inResponseTo: samlRequestId }) })
.expect(302);
// User should be redirected to the URL that initiated handshake.
expect(samlAuthenticationResponse.headers.location).to.be(
- '/abc/xyz/handshake?one=two%20three#/workpad'
+ '/abc/xyz/handshake?one=two+three#/workpad'
);
const cookies = samlAuthenticationResponse.headers['set-cookie'];
@@ -221,7 +217,6 @@ export default function ({ getService }: FtrProviderContext) {
// Don't pass handshake cookie and don't include `inResponseTo` into SAML response to simulate IdP initiated login.
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.send({ SAMLResponse: await createSAMLResponse() })
.expect(302);
@@ -235,14 +230,18 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should fail if SAML response is not valid', async () => {
- await supertest
+ const unauthenticatedResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.send({
SAMLResponse: await createSAMLResponse({ inResponseTo: 'some-invalid-request-id' }),
})
.expect(401);
+
+ expect(unauthenticatedResponse.headers['content-security-policy']).to.be(
+ `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'`
+ );
+ expect(unauthenticatedResponse.text).to.contain('We couldn't log you in');
});
});
@@ -253,7 +252,6 @@ export default function ({ getService }: FtrProviderContext) {
// Imitate IdP initiated login.
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.send({ SAMLResponse: await createSAMLResponse() })
.expect(302);
@@ -315,23 +313,17 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const samlRequestId = await getSAMLRequestId(handshakeResponse.body.location);
+ const samlRequestId = await getSAMLRequestId(handshakeResponse.headers.location);
idpSessionIndex = String(randomness.naturalNumber());
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.send({
SAMLResponse: await createSAMLResponse({
@@ -372,11 +364,11 @@ export default function ({ getService }: FtrProviderContext) {
.expect(401);
});
- it('should redirect to home page if session cookie is not provided', async () => {
+ it('should redirect to `logged_out` page if session cookie is not provided', async () => {
const logoutResponse = await supertest.get('/api/security/logout').expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
- expect(logoutResponse.headers.location).to.be('/');
+ expect(logoutResponse.headers.location).to.be('/security/logged_out?msg=LOGGED_OUT');
});
it('should reject AJAX requests', async () => {
@@ -459,22 +451,16 @@ export default function ({ getService }: FtrProviderContext) {
this.timeout(40000);
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const samlRequestId = await getSAMLRequestId(handshakeResponse.body.location);
+ const samlRequestId = await getSAMLRequestId(handshakeResponse.headers.location);
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.send({ SAMLResponse: await createSAMLResponse({ inResponseTo: samlRequestId }) })
.expect(302);
@@ -559,22 +545,16 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL:
- 'https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad',
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const samlRequestId = await getSAMLRequestId(handshakeResponse.body.location);
+ const samlRequestId = await getSAMLRequestId(handshakeResponse.headers.location);
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.send({ SAMLResponse: await createSAMLResponse({ inResponseTo: samlRequestId }) })
.expect(302);
@@ -609,21 +589,16 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeCookie.maxAge).to.be(0);
expect(handshakeResponse.headers.location).to.be(
- '/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml'
+ '/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2Bthree%26auth_provider_hint%3Dsaml'
);
});
it('should properly set cookie and redirect user to IdP', async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .set('Cookie', sessionCookie.cookieString())
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL: `https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad`,
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const cookies = handshakeResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
@@ -634,7 +609,10 @@ export default function ({ getService }: FtrProviderContext) {
expect(handshakeCookie.path).to.be('/');
expect(handshakeCookie.httpOnly).to.be(true);
- const redirectURL = url.parse(handshakeResponse.body.location, true /* parseQueryString */);
+ const redirectURL = url.parse(
+ handshakeResponse.headers.location,
+ true /* parseQueryString */
+ );
expect(redirectURL.href!.startsWith(`https://elastic.co/sso/saml`)).to.be(true);
expect(redirectURL.query.SAMLRequest).to.not.be.empty();
});
@@ -680,21 +658,16 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const handshakeResponse = await supertest
- .post('/internal/security/login')
- .set('kbn-xsrf', 'xxx')
- .send({
- providerType: 'saml',
- providerName: 'saml',
- currentURL: `https://kibana.com/internal/security/capture-url?next=%2Fabc%2Fxyz%2Fhandshake%3Fone%3Dtwo%2520three&providerType=saml&providerName=saml#/workpad`,
- })
- .expect(200);
+ .get(
+ '/abc/xyz/handshake?one=two three&auth_provider_hint=saml&auth_url_hash=%23%2Fworkpad'
+ )
+ .expect(302);
const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!;
- const samlRequestId = await getSAMLRequestId(handshakeResponse.body.location);
+ const samlRequestId = await getSAMLRequestId(handshakeResponse.headers.location);
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', handshakeCookie.cookieString())
.send({
SAMLResponse: await createSAMLResponse({
@@ -715,7 +688,6 @@ export default function ({ getService }: FtrProviderContext) {
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', existingSessionCookie.cookieString())
.send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) })
.expect(302);
@@ -745,7 +717,6 @@ export default function ({ getService }: FtrProviderContext) {
const newUsername = 'c@d.e';
const samlAuthenticationResponse = await supertest
.post('/api/security/saml/callback')
- .set('kbn-xsrf', 'xxx')
.set('Cookie', existingSessionCookie.cookieString())
.send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) })
.expect(302);
diff --git a/x-pack/test/security_functional/fixtures/common/test_endpoints/kibana.json b/x-pack/test/security_functional/fixtures/common/test_endpoints/kibana.json
new file mode 100644
index 0000000000000..89b7725fe2b4e
--- /dev/null
+++ b/x-pack/test/security_functional/fixtures/common/test_endpoints/kibana.json
@@ -0,0 +1,7 @@
+{
+ "id": "securityTestEndpoints",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": true
+}
diff --git a/x-pack/test/security_functional/fixtures/common/test_endpoints/public/index.ts b/x-pack/test/security_functional/fixtures/common/test_endpoints/public/index.ts
new file mode 100644
index 0000000000000..d4ed6597651fb
--- /dev/null
+++ b/x-pack/test/security_functional/fixtures/common/test_endpoints/public/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TestEndpointsPlugin } from './plugin';
+export const plugin = () => new TestEndpointsPlugin();
diff --git a/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx b/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx
new file mode 100644
index 0000000000000..2796d64c01a58
--- /dev/null
+++ b/x-pack/test/security_functional/fixtures/common/test_endpoints/public/plugin.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { CoreSetup, Plugin } from 'src/core/public';
+import ReactDOM from 'react-dom';
+import React from 'react';
+
+export class TestEndpointsPlugin implements Plugin {
+ public setup(core: CoreSetup) {
+ core.application.register({
+ id: 'authentication_app',
+ title: 'Authentication app',
+ appRoute: '/authentication/app',
+ async mount({ element }) {
+ ReactDOM.render(
+ Authenticated!
,
+ element
+ );
+ return () => ReactDOM.unmountComponentAtNode(element);
+ },
+ });
+ }
+ public start() {}
+ public stop() {}
+}
diff --git a/x-pack/test/security_functional/fixtures/common/test_endpoints/server/index.ts b/x-pack/test/security_functional/fixtures/common/test_endpoints/server/index.ts
new file mode 100644
index 0000000000000..4ae669256d3a8
--- /dev/null
+++ b/x-pack/test/security_functional/fixtures/common/test_endpoints/server/index.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { PluginInitializer } from '../../../../../../../src/core/server';
+import { initRoutes } from './init_routes';
+
+export const plugin: PluginInitializer = () => ({
+ setup: (core) => initRoutes(core),
+ start: () => {},
+ stop: () => {},
+});
diff --git a/x-pack/test/security_functional/fixtures/common/test_endpoints/server/init_routes.ts b/x-pack/test/security_functional/fixtures/common/test_endpoints/server/init_routes.ts
new file mode 100644
index 0000000000000..378ab8a90c29a
--- /dev/null
+++ b/x-pack/test/security_functional/fixtures/common/test_endpoints/server/init_routes.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { CoreSetup } from '../../../../../../../src/core/server';
+
+export function initRoutes(core: CoreSetup) {
+ const authenticationAppOptions = { simulateUnauthorized: false };
+ core.http.resources.register(
+ {
+ path: '/authentication/app',
+ validate: false,
+ },
+ async (context, request, response) => {
+ if (authenticationAppOptions.simulateUnauthorized) {
+ return response.unauthorized();
+ }
+ return response.renderCoreApp();
+ }
+ );
+
+ const router = core.http.createRouter();
+ router.post(
+ {
+ path: '/authentication/app/setup',
+ validate: { body: schema.object({ simulateUnauthorized: schema.boolean() }) },
+ options: { authRequired: false, xsrfRequired: false },
+ },
+ (context, request, response) => {
+ authenticationAppOptions.simulateUnauthorized = request.body.simulateUnauthorized;
+ return response.ok();
+ }
+ );
+}
diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts
index 39f797c488e00..6f7a2a8b3de70 100644
--- a/x-pack/test/security_functional/login_selector.config.ts
+++ b/x-pack/test/security_functional/login_selector.config.ts
@@ -30,6 +30,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'../security_api_integration/fixtures/saml/saml_provider'
);
+ const testEndpointsPlugin = resolve(__dirname, './fixtures/common/test_endpoints');
+
return {
testFiles: [resolve(__dirname, './tests/login_selector')],
@@ -59,6 +61,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
serverArgs: [
...kibanaCommonConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${samlIdPPlugin}`,
+ `--plugin-path=${testEndpointsPlugin}`,
'--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d',
'--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
`--xpack.security.loginHelp="Some-login-help."`,
diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts
index dcf9177eb52b1..056d7b8197c9a 100644
--- a/x-pack/test/security_functional/oidc.config.ts
+++ b/x-pack/test/security_functional/oidc.config.ts
@@ -27,6 +27,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'../security_api_integration/fixtures/oidc/oidc_provider'
);
+ const testEndpointsPlugin = resolve(__dirname, './fixtures/common/test_endpoints');
+
return {
testFiles: [resolve(__dirname, './tests/oidc')],
@@ -61,6 +63,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
serverArgs: [
...kibanaCommonConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${oidcOpPPlugin}`,
+ `--plugin-path=${testEndpointsPlugin}`,
'--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d',
'--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.security.authc.selector.enabled=false',
diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts
index 3715b826824e1..50948ce6d411e 100644
--- a/x-pack/test/security_functional/saml.config.ts
+++ b/x-pack/test/security_functional/saml.config.ts
@@ -30,6 +30,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'../security_api_integration/fixtures/saml/saml_provider'
);
+ const testEndpointsPlugin = resolve(__dirname, './fixtures/common/test_endpoints');
+
return {
testFiles: [resolve(__dirname, './tests/saml')],
@@ -58,6 +60,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
serverArgs: [
...kibanaCommonConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${samlIdPPlugin}`,
+ `--plugin-path=${testEndpointsPlugin}`,
'--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d',
'--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.security.authc.selector.enabled=false',
diff --git a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts
index 1561aa6cb47fd..ca6b9497b194e 100644
--- a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts
+++ b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts
@@ -20,17 +20,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('Basic functionality', function () {
this.tags('includeFirefox');
+ const testCredentials = { username: 'admin_user', password: 'change_me' };
before(async () => {
await getService('esSupertest')
.post('/_security/role_mapping/saml1')
.send({ roles: ['superuser'], enabled: true, rules: { field: { 'realm.name': 'saml1' } } })
.expect(200);
+ await security.user.create(testCredentials.username, {
+ password: testCredentials.password,
+ roles: ['kibana_admin'],
+ full_name: 'Admin',
+ });
+
await esArchiver.load('../../functional/es_archives/empty_kibana');
await PageObjects.security.forceLogout();
});
after(async () => {
+ await security.user.delete(testCredentials.username);
await esArchiver.unload('../../functional/es_archives/empty_kibana');
});
@@ -95,6 +103,58 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(currentURL.pathname).to.eql('/app/management/security/users');
});
+ it('can login after `Unauthorized` error after request authentication preserving original URL', async () => {
+ await getService('supertest')
+ .post('/authentication/app/setup')
+ .send({ simulateUnauthorized: true })
+ .expect(200);
+ await PageObjects.security.loginSelector.login('basic', 'basic1');
+ await browser.get(`${deployment.getHostPort()}/authentication/app?one=two`);
+
+ await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible();
+ expect(await PageObjects.security.loginPage.getErrorMessage()).to.be(
+ "We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator."
+ );
+
+ await getService('supertest')
+ .post('/authentication/app/setup')
+ .send({ simulateUnauthorized: false })
+ .expect(200);
+ await PageObjects.security.loginSelector.login('basic', 'basic1');
+
+ const currentURL = parse(await browser.getCurrentUrl());
+ expect(currentURL.path).to.eql('/authentication/app?one=two');
+ });
+
+ it('can login after `Unauthorized` error during request authentication preserving original URL', async () => {
+ // 1. Navigate to Kibana to make sure user is properly authenticated.
+ await PageObjects.common.navigateToUrl('management', 'security/users', {
+ ensureCurrentUrl: false,
+ shouldLoginIfPrompted: false,
+ shouldUseHashForSubUrl: false,
+ });
+ await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible();
+ await PageObjects.security.loginSelector.login('basic', 'basic1', testCredentials);
+ expect(parse(await browser.getCurrentUrl()).pathname).to.eql(
+ '/app/management/security/users'
+ );
+
+ // 2. Now disable user and try to refresh page causing authentication to fail.
+ await security.user.disable(testCredentials.username);
+ await browser.refresh();
+ await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible();
+ expect(await PageObjects.security.loginPage.getErrorMessage()).to.be(
+ "We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator."
+ );
+
+ // 3. Re-enable user and try to login again.
+ await security.user.enable(testCredentials.username);
+ await PageObjects.security.loginSelector.login('basic', 'basic1', testCredentials);
+ expect(parse(await browser.getCurrentUrl()).pathname).to.eql(
+ '/app/management/security/users'
+ );
+ });
+
it('should show toast with error if SSO fails', async () => {
await PageObjects.security.loginSelector.selectLoginMethod('saml', 'unknown_saml');
diff --git a/x-pack/test/security_functional/tests/oidc/url_capture.ts b/x-pack/test/security_functional/tests/oidc/url_capture.ts
index 968e1001a5f56..b72aab33034c5 100644
--- a/x-pack/test/security_functional/tests/oidc/url_capture.ts
+++ b/x-pack/test/security_functional/tests/oidc/url_capture.ts
@@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const browser = getService('browser');
const deployment = getService('deployment');
+ const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('URL capture', function () {
@@ -52,5 +53,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(currentURL.pathname).to.eql('/app/management/security/users');
expect(currentURL.hash).to.eql('#some=hash-value');
});
+
+ it('can login after `Unauthorized` error preserving original URL', async () => {
+ await getService('supertest')
+ .post('/authentication/app/setup')
+ .send({ simulateUnauthorized: true })
+ .expect(200);
+ await browser.get(`${deployment.getHostPort()}/authentication/app?one=two`);
+
+ await find.byCssSelector('[data-test-subj="promptPage"]', 20000);
+ await getService('supertest')
+ .post('/authentication/app/setup')
+ .send({ simulateUnauthorized: false })
+ .expect(200);
+
+ await testSubjects.click('logInButton');
+ await find.byCssSelector('[data-test-subj="testEndpointsAuthenticationApp"]', 20000);
+
+ const currentURL = parse(await browser.getCurrentUrl());
+ expect(currentURL.path).to.eql('/authentication/app?one=two');
+ });
});
}
diff --git a/x-pack/test/security_functional/tests/saml/url_capture.ts b/x-pack/test/security_functional/tests/saml/url_capture.ts
index 80f6528334cc0..09eee12813601 100644
--- a/x-pack/test/security_functional/tests/saml/url_capture.ts
+++ b/x-pack/test/security_functional/tests/saml/url_capture.ts
@@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const browser = getService('browser');
const deployment = getService('deployment');
+ const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('URL capture', function () {
@@ -52,5 +53,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(currentURL.pathname).to.eql('/app/management/security/users');
expect(currentURL.hash).to.eql('#some=hash-value');
});
+
+ it('can login after `Unauthorized` error preserving original URL', async () => {
+ await getService('supertest')
+ .post('/authentication/app/setup')
+ .send({ simulateUnauthorized: true })
+ .expect(200);
+ await browser.get(`${deployment.getHostPort()}/authentication/app?one=two`);
+
+ await find.byCssSelector('[data-test-subj="promptPage"]', 20000);
+ await getService('supertest')
+ .post('/authentication/app/setup')
+ .send({ simulateUnauthorized: false })
+ .expect(200);
+
+ await testSubjects.click('logInButton');
+ await find.byCssSelector('[data-test-subj="testEndpointsAuthenticationApp"]', 20000);
+
+ const currentURL = parse(await browser.getCurrentUrl());
+ expect(currentURL.path).to.eql('/authentication/app?one=two');
+ });
});
}
From 35bd88e754e5b813d474eb0d97a824816fe10f02 Mon Sep 17 00:00:00 2001
From: Domenico Andreoli
Date: Tue, 27 Apr 2021 19:12:23 +0200
Subject: [PATCH 08/70] [DOCS] Fix development tests table (#98309)
* Fix visualization of consecutive inline code blocks
* Remove trailing '_' from the correct script name
---
docs/developer/contributing/development-tests.asciidoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc
index 7aabc480cdaa2..715b1a15ab5ed 100644
--- a/docs/developer/contributing/development-tests.asciidoc
+++ b/docs/developer/contributing/development-tests.asciidoc
@@ -19,7 +19,7 @@ root)
|Functional
|`test/**/config.js` `x-pack/test/**/config.js`
-|`node scripts/functional_tests_server --config [directory]/config.js``node scripts/functional_test_runner_ --config [directory]/config.js --grep=regexp`
+|`node scripts/functional_tests_server --config [directory]/config.js` `node scripts/functional_test_runner --config [directory]/config.js --grep=regexp`
|===
Test runner arguments: - Where applicable, the optional arguments
From b0486caefdbdb0dc7a7f80082d030bded8d46ba2 Mon Sep 17 00:00:00 2001
From: Melissa Alvarez
Date: Tue, 27 Apr 2021 13:21:01 -0400
Subject: [PATCH 09/70] ensure map supports old source index metadata (#98483)
---
.../ml/server/models/data_frame_analytics/analytics_manager.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts
index 7deb392467df5..a9c1d95d933a9 100644
--- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts
+++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_manager.ts
@@ -445,7 +445,8 @@ export class AnalyticsManager {
// Check meta data
if (
link.isWildcardIndexPattern === false &&
- (link.meta === undefined || link.meta?.created_by === INDEX_META_DATA_CREATED_BY)
+ (link.meta === undefined ||
+ link.meta?.created_by.includes(INDEX_META_DATA_CREATED_BY))
) {
rootIndexPattern = nextLinkId;
complete = true;
From 7c043dc3dea8c92d98c9c80e7177fee9e9fe1eac Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Tue, 27 Apr 2021 18:44:41 +0100
Subject: [PATCH 10/70] chore(NA): moving @kbn/apm-config-loader into bazel
(#98323)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../monorepo-packages.asciidoc | 1 +
package.json | 2 +-
packages/BUILD.bazel | 1 +
packages/kbn-apm-config-loader/BUILD.bazel | 87 +++++++++++++++++++
packages/kbn-apm-config-loader/package.json | 7 +-
packages/kbn-apm-config-loader/tsconfig.json | 3 +-
yarn.lock | 2 +-
7 files changed, 94 insertions(+), 9 deletions(-)
create mode 100644 packages/kbn-apm-config-loader/BUILD.bazel
diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc
index 6e2d41e5ed679..d2c24a9e4944f 100644
--- a/docs/developer/getting-started/monorepo-packages.asciidoc
+++ b/docs/developer/getting-started/monorepo-packages.asciidoc
@@ -63,6 +63,7 @@ yarn kbn watch-bazel
- @elastic/datemath
- @elastic/safer-lodash-set
+- @kbn/apm-config-loader
- @kbn/apm-utils
- @kbn/babel-code-parser
- @kbn/babel-preset
diff --git a/package.json b/package.json
index 24355e25f6a8b..0a23be6779a7d 100644
--- a/package.json
+++ b/package.json
@@ -123,7 +123,7 @@
"@hapi/wreck": "^17.1.0",
"@kbn/ace": "link:packages/kbn-ace",
"@kbn/analytics": "link:packages/kbn-analytics",
- "@kbn/apm-config-loader": "link:packages/kbn-apm-config-loader",
+ "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader/npm_module",
"@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module",
"@kbn/config": "link:packages/kbn-config",
"@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 902c2804ee012..a355d537f06a1 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -5,6 +5,7 @@ filegroup(
srcs = [
"//packages/elastic-datemath:build",
"//packages/elastic-safer-lodash-set:build",
+ "//packages/kbn-apm-config-loader:build",
"//packages/kbn-apm-utils:build",
"//packages/kbn-babel-code-parser:build",
"//packages/kbn-babel-preset:build",
diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel
new file mode 100644
index 0000000000000..58a86ccfcf018
--- /dev/null
+++ b/packages/kbn-apm-config-loader/BUILD.bazel
@@ -0,0 +1,87 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+
+PKG_BASE_NAME = "kbn-apm-config-loader"
+PKG_REQUIRE_NAME = "@kbn/apm-config-loader"
+
+SOURCE_FILES = glob(
+ [
+ "src/**/*.ts",
+ ],
+ exclude = [
+ "**/*.test.*"
+ ],
+)
+
+SRCS = SOURCE_FILES
+
+filegroup(
+ name = "srcs",
+ srcs = SRCS,
+)
+
+NPM_MODULE_EXTRA_FILES = [
+ "package.json",
+ "README.md"
+]
+
+SRC_DEPS = [
+ "//packages/elastic-safer-lodash-set",
+ "//packages/kbn-utils",
+ "@npm//js-yaml",
+ "@npm//lodash",
+]
+
+TYPES_DEPS = [
+ "@npm//@types/jest",
+ "@npm//@types/js-yaml",
+ "@npm//@types/lodash",
+ "@npm//@types/node",
+]
+
+DEPS = SRC_DEPS + TYPES_DEPS
+
+ts_config(
+ name = "tsconfig",
+ src = "tsconfig.json",
+ deps = [
+ "//:tsconfig.base.json",
+ ],
+)
+
+ts_project(
+ name = "tsc",
+ args = ['--pretty'],
+ srcs = SRCS,
+ deps = DEPS,
+ declaration = True,
+ declaration_map = True,
+ incremental = True,
+ out_dir = "target",
+ source_map = True,
+ root_dir = "src",
+ tsconfig = ":tsconfig",
+)
+
+js_library(
+ name = PKG_BASE_NAME,
+ srcs = NPM_MODULE_EXTRA_FILES,
+ deps = [":tsc"] + DEPS,
+ package_name = PKG_REQUIRE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+pkg_npm(
+ name = "npm_module",
+ deps = [
+ ":%s" % PKG_BASE_NAME,
+ ]
+)
+
+filegroup(
+ name = "build",
+ srcs = [
+ ":npm_module",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-apm-config-loader/package.json b/packages/kbn-apm-config-loader/package.json
index b9dc324ec5e78..c096ed2efb92a 100644
--- a/packages/kbn-apm-config-loader/package.json
+++ b/packages/kbn-apm-config-loader/package.json
@@ -4,10 +4,5 @@
"types": "./target/index.d.ts",
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0",
- "private": true,
- "scripts": {
- "build": "../../node_modules/.bin/tsc",
- "kbn:bootstrap": "yarn build",
- "kbn:watch": "yarn build --watch"
- }
+ "private": true
}
\ No newline at end of file
diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json
index 250195785b931..aa34b05061600 100644
--- a/packages/kbn-apm-config-loader/tsconfig.json
+++ b/packages/kbn-apm-config-loader/tsconfig.json
@@ -1,11 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
- "incremental": false,
+ "incremental": true,
"outDir": "./target",
"stripInternal": false,
"declaration": true,
"declarationMap": true,
+ "rootDir": "./src",
"sourceMap": true,
"sourceRoot": "../../../../packages/kbn-apm-config-loader/src",
"types": [
diff --git a/yarn.lock b/yarn.lock
index 133f21a200f70..9302839004a33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2577,7 +2577,7 @@
version "0.0.0"
uid ""
-"@kbn/apm-config-loader@link:packages/kbn-apm-config-loader":
+"@kbn/apm-config-loader@link:bazel-bin/packages/kbn-apm-config-loader/npm_module":
version "0.0.0"
uid ""
From afc4aef9d59074ba2f3a491ef6e0de223505aa73 Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Tue, 27 Apr 2021 20:01:07 +0200
Subject: [PATCH 11/70] Cleanup UI settings client API (#98248)
* remove unused overrideLocalDefault method
* remove unused getSasved$ method
* fix dashboard unit test
* update docs
* fix Security Solution unit test
---
...core-public.iuisettingsclient.getsaved_.md | 17 ---
...na-plugin-core-public.iuisettingsclient.md | 2 -
....iuisettingsclient.overridelocaldefault.md | 13 --
src/core/public/public.api.md | 6 -
.../ui_settings_client.test.ts.snap | 75 -----------
src/core/public/ui_settings/types.ts | 16 ---
.../ui_settings/ui_settings_client.test.ts | 116 ------------------
.../public/ui_settings/ui_settings_client.ts | 30 -----
.../ui_settings/ui_settings_service.mock.ts | 3 -
.../ui_settings/ui_settings_service.test.ts | 7 +-
.../management_app/advanced_settings.test.tsx | 1 -
.../dashboard_empty_screen.test.tsx.snap | 6 -
.../header/__snapshots__/index.test.tsx.snap | 2 -
13 files changed, 1 insertion(+), 293 deletions(-)
delete mode 100644 docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getsaved_.md
delete mode 100644 docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.overridelocaldefault.md
diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getsaved_.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getsaved_.md
deleted file mode 100644
index 953bb75625c97..0000000000000
--- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.getsaved_.md
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) > [getSaved$](./kibana-plugin-core-public.iuisettingsclient.getsaved_.md)
-
-## IUiSettingsClient.getSaved$ property
-
-Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed.
-
-Signature:
-
-```typescript
-getSaved$: () => Observable<{
- key: string;
- newValue: T;
- oldValue: T;
- }>;
-```
diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md
index 87ef5784a6c6d..d6f3b3186b542 100644
--- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md
+++ b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.md
@@ -19,14 +19,12 @@ export interface IUiSettingsClient
| [get](./kibana-plugin-core-public.iuisettingsclient.get.md) | <T = any>(key: string, defaultOverride?: T) => T
| Gets the value for a specific uiSetting. If this setting has no user-defined value then the defaultOverride
parameter is returned (and parsed if setting is of type "json" or "number). If the parameter is not defined and the key is not registered by any plugin then an error is thrown, otherwise reads the default value defined by a plugin. |
| [get$](./kibana-plugin-core-public.iuisettingsclient.get_.md) | <T = any>(key: string, defaultOverride?: T) => Observable<T>
| Gets an observable of the current value for a config key, and all updates to that config key in the future. Providing a defaultOverride
argument behaves the same as it does in \#get() |
| [getAll](./kibana-plugin-core-public.iuisettingsclient.getall.md) | () => Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>>
| Gets the metadata about all uiSettings, including the type, default value, and user value for each key. |
-| [getSaved$](./kibana-plugin-core-public.iuisettingsclient.getsaved_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}>
| Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. |
| [getUpdate$](./kibana-plugin-core-public.iuisettingsclient.getupdate_.md) | <T = any>() => Observable<{
key: string;
newValue: T;
oldValue: T;
}>
| Returns an Observable that notifies subscribers of each update to the uiSettings, including the key, newValue, and oldValue of the setting that changed. |
| [getUpdateErrors$](./kibana-plugin-core-public.iuisettingsclient.getupdateerrors_.md) | () => Observable<Error>
| Returns an Observable that notifies subscribers of each error while trying to update the settings, containing the actual Error class. |
| [isCustom](./kibana-plugin-core-public.iuisettingsclient.iscustom.md) | (key: string) => boolean
| Returns true if the setting wasn't registered by any plugin, but was either added directly via set()
, or is an unknown setting found in the uiSettings saved object |
| [isDeclared](./kibana-plugin-core-public.iuisettingsclient.isdeclared.md) | (key: string) => boolean
| Returns true if the key is a "known" uiSetting, meaning it is either registered by any plugin or was previously added as a custom setting via the set()
method. |
| [isDefault](./kibana-plugin-core-public.iuisettingsclient.isdefault.md) | (key: string) => boolean
| Returns true if the setting has no user-defined value or is unknown |
| [isOverridden](./kibana-plugin-core-public.iuisettingsclient.isoverridden.md) | (key: string) => boolean
| Shows whether the uiSettings value set by the user. |
-| [overrideLocalDefault](./kibana-plugin-core-public.iuisettingsclient.overridelocaldefault.md) | (key: string, newDefault: any) => void
| Overrides the default value for a setting in this specific browser tab. If the page is reloaded the default override is lost. |
| [remove](./kibana-plugin-core-public.iuisettingsclient.remove.md) | (key: string) => Promise<boolean>
| Removes the user-defined value for a setting, causing it to revert to the default. This method behaves the same as calling set(key, null)
, including the synchronization, custom setting, and error behavior of that method. |
| [set](./kibana-plugin-core-public.iuisettingsclient.set.md) | (key: string, value: any) => Promise<boolean>
| Sets the value for a uiSetting. If the setting is not registered by any plugin it will be stored as a custom setting. The new value will be synchronously available via the get()
method and sent to the server in the background. If the request to the server fails then a updateErrors$ will be notified and the setting will be reverted to its value before set()
was called. |
diff --git a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.overridelocaldefault.md b/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.overridelocaldefault.md
deleted file mode 100644
index 0ae52e4959e10..0000000000000
--- a/docs/development/core/public/kibana-plugin-core-public.iuisettingsclient.overridelocaldefault.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) > [overrideLocalDefault](./kibana-plugin-core-public.iuisettingsclient.overridelocaldefault.md)
-
-## IUiSettingsClient.overrideLocalDefault property
-
-Overrides the default value for a setting in this specific browser tab. If the page is reloaded the default override is lost.
-
-Signature:
-
-```typescript
-overrideLocalDefault: (key: string, newDefault: any) => void;
-```
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 26e2986abb928..1f502007f51dd 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -911,11 +911,6 @@ export interface IUiSettingsClient {
get$: (key: string, defaultOverride?: T) => Observable;
get: (key: string, defaultOverride?: T) => T;
getAll: () => Readonly>;
- getSaved$: () => Observable<{
- key: string;
- newValue: T;
- oldValue: T;
- }>;
getUpdate$: () => Observable<{
key: string;
newValue: T;
@@ -926,7 +921,6 @@ export interface IUiSettingsClient {
isDeclared: (key: string) => boolean;
isDefault: (key: string) => boolean;
isOverridden: (key: string) => boolean;
- overrideLocalDefault: (key: string, newDefault: any) => void;
remove: (key: string) => Promise;
set: (key: string, value: any) => Promise;
}
diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap
index cd233704d2f54..b9526f26a0c1e 100644
--- a/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap
+++ b/src/core/public/ui_settings/__snapshots__/ui_settings_client.test.ts.snap
@@ -44,81 +44,6 @@ Array [
]
`;
-exports[`#overrideLocalDefault key has no user value calls subscriber with new and previous value: single subscriber call 1`] = `
-Array [
- Array [
- Object {
- "key": "dateFormat",
- "newValue": "bar",
- "oldValue": "Browser",
- },
- ],
-]
-`;
-
-exports[`#overrideLocalDefault key has no user value synchronously modifies the default value returned by get(): get after override 1`] = `"bar"`;
-
-exports[`#overrideLocalDefault key has no user value synchronously modifies the default value returned by get(): get before override 1`] = `"Browser"`;
-
-exports[`#overrideLocalDefault key has no user value synchronously modifies the value returned by getAll(): getAll after override 1`] = `
-Object {
- "dateFormat": Object {
- "value": "bar",
- },
-}
-`;
-
-exports[`#overrideLocalDefault key has no user value synchronously modifies the value returned by getAll(): getAll before override 1`] = `
-Object {
- "dateFormat": Object {
- "value": "Browser",
- },
-}
-`;
-
-exports[`#overrideLocalDefault key with user value does not modify the return value of get: get after override 1`] = `"foo"`;
-
-exports[`#overrideLocalDefault key with user value does not modify the return value of get: get before override 1`] = `"foo"`;
-
-exports[`#overrideLocalDefault key with user value is included in the return value of getAll: getAll after override 1`] = `
-Object {
- "dateFormat": Object {
- "userValue": "foo",
- "value": "bar",
- },
-}
-`;
-
-exports[`#overrideLocalDefault key with user value is included in the return value of getAll: getAll before override 1`] = `
-Object {
- "dateFormat": Object {
- "userValue": "foo",
- "value": "Browser",
- },
-}
-`;
-
-exports[`#overrideLocalDefault key with user value returns default override when setting removed: get after override 1`] = `"bar"`;
-
-exports[`#overrideLocalDefault key with user value returns default override when setting removed: get before override 1`] = `"foo"`;
-
-exports[`#overrideLocalDefault key with user value returns default override when setting removed: getAll after override 1`] = `
-Object {
- "dateFormat": Object {
- "value": "bar",
- },
-}
-`;
-
-exports[`#overrideLocalDefault key with user value returns default override when setting removed: getAll before override 1`] = `
-Object {
- "dateFormat": Object {
- "userValue": "foo",
- "value": "bar",
- },
-}
-`;
-
exports[`#remove throws an error if key is overridden 1`] = `"Unable to update \\"bar\\" because its value is overridden by the Kibana server"`;
exports[`#set throws an error if key is overridden 1`] = `"Unable to update \\"foo\\" because its value is overridden by the Kibana server"`;
diff --git a/src/core/public/ui_settings/types.ts b/src/core/public/ui_settings/types.ts
index 4ae09094861e4..77c1feb25b6bc 100644
--- a/src/core/public/ui_settings/types.ts
+++ b/src/core/public/ui_settings/types.ts
@@ -53,12 +53,6 @@ export interface IUiSettingsClient {
*/
set: (key: string, value: any) => Promise;
- /**
- * Overrides the default value for a setting in this specific browser tab. If the page
- * is reloaded the default override is lost.
- */
- overrideLocalDefault: (key: string, newDefault: any) => void;
-
/**
* Removes the user-defined value for a setting, causing it to revert to the default. This
* method behaves the same as calling `set(key, null)`, including the synchronization, custom
@@ -99,16 +93,6 @@ export interface IUiSettingsClient {
oldValue: T;
}>;
- /**
- * Returns an Observable that notifies subscribers of each update to the uiSettings,
- * including the key, newValue, and oldValue of the setting that changed.
- */
- getSaved$: () => Observable<{
- key: string;
- newValue: T;
- oldValue: T;
- }>;
-
/**
* Returns an Observable that notifies subscribers of each error while trying to update
* the settings, containing the actual Error class.
diff --git a/src/core/public/ui_settings/ui_settings_client.test.ts b/src/core/public/ui_settings/ui_settings_client.test.ts
index 40e04a46c0001..f8c5dbfc347dd 100644
--- a/src/core/public/ui_settings/ui_settings_client.test.ts
+++ b/src/core/public/ui_settings/ui_settings_client.test.ts
@@ -279,119 +279,3 @@ describe('#getUpdate$', () => {
expect(onComplete).toHaveBeenCalled();
});
});
-
-describe('#overrideLocalDefault', () => {
- describe('key has no user value', () => {
- it('synchronously modifies the default value returned by get()', () => {
- const { client } = setup();
-
- expect(client.get('dateFormat')).toMatchSnapshot('get before override');
- client.overrideLocalDefault('dateFormat', 'bar');
- expect(client.get('dateFormat')).toMatchSnapshot('get after override');
- });
-
- it('synchronously modifies the value returned by getAll()', () => {
- const { client } = setup();
-
- expect(client.getAll()).toMatchSnapshot('getAll before override');
- client.overrideLocalDefault('dateFormat', 'bar');
- expect(client.getAll()).toMatchSnapshot('getAll after override');
- });
-
- it('calls subscriber with new and previous value', () => {
- const handler = jest.fn();
- const { client } = setup();
-
- client.getUpdate$().subscribe(handler);
- client.overrideLocalDefault('dateFormat', 'bar');
- expect(handler.mock.calls).toMatchSnapshot('single subscriber call');
- });
- });
-
- describe('key with user value', () => {
- it('does not modify the return value of get', () => {
- const { client } = setup();
-
- client.set('dateFormat', 'foo');
- expect(client.get('dateFormat')).toMatchSnapshot('get before override');
- client.overrideLocalDefault('dateFormat', 'bar');
- expect(client.get('dateFormat')).toMatchSnapshot('get after override');
- });
-
- it('is included in the return value of getAll', () => {
- const { client } = setup();
-
- client.set('dateFormat', 'foo');
- expect(client.getAll()).toMatchSnapshot('getAll before override');
- client.overrideLocalDefault('dateFormat', 'bar');
- expect(client.getAll()).toMatchSnapshot('getAll after override');
- });
-
- it('does not call subscriber', () => {
- const handler = jest.fn();
- const { client } = setup();
-
- client.set('dateFormat', 'foo');
- client.getUpdate$().subscribe(handler);
- client.overrideLocalDefault('dateFormat', 'bar');
- expect(handler).not.toHaveBeenCalled();
- });
-
- it('returns default override when setting removed', () => {
- const { client } = setup();
-
- client.set('dateFormat', 'foo');
- client.overrideLocalDefault('dateFormat', 'bar');
-
- expect(client.get('dateFormat')).toMatchSnapshot('get before override');
- expect(client.getAll()).toMatchSnapshot('getAll before override');
-
- client.remove('dateFormat');
-
- expect(client.get('dateFormat')).toMatchSnapshot('get after override');
- expect(client.getAll()).toMatchSnapshot('getAll after override');
- });
- });
-
- describe('#isOverridden()', () => {
- it('returns false if key is unknown', () => {
- const { client } = setup();
- expect(client.isOverridden('foo')).toBe(false);
- });
-
- it('returns false if key is no overridden', () => {
- const { client } = setup({
- initialSettings: {
- foo: {
- userValue: 1,
- },
- bar: {
- isOverridden: true,
- userValue: 2,
- },
- },
- });
- expect(client.isOverridden('foo')).toBe(false);
- });
-
- it('returns true when key is overridden', () => {
- const { client } = setup({
- initialSettings: {
- foo: {
- userValue: 1,
- },
- bar: {
- isOverridden: true,
- userValue: 2,
- },
- },
- });
- expect(client.isOverridden('bar')).toBe(true);
- });
-
- it('returns false for object prototype properties', () => {
- const { client } = setup();
- expect(client.isOverridden('hasOwnProperty')).toBe(false);
- });
- });
-});
diff --git a/src/core/public/ui_settings/ui_settings_client.ts b/src/core/public/ui_settings/ui_settings_client.ts
index ab7c91803549b..ee5d5da8d29b9 100644
--- a/src/core/public/ui_settings/ui_settings_client.ts
+++ b/src/core/public/ui_settings/ui_settings_client.ts
@@ -24,7 +24,6 @@ interface UiSettingsClientParams {
export class UiSettingsClient implements IUiSettingsClient {
private readonly update$ = new Subject<{ key: string; newValue: any; oldValue: any }>();
- private readonly saved$ = new Subject<{ key: string; newValue: any; oldValue: any }>();
private readonly updateErrors$ = new Subject();
private readonly api: UiSettingsApi;
@@ -39,7 +38,6 @@ export class UiSettingsClient implements IUiSettingsClient {
params.done$.subscribe({
complete: () => {
this.update$.complete();
- this.saved$.complete();
this.updateErrors$.complete();
},
});
@@ -116,37 +114,10 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r
return this.isDeclared(key) && Boolean(this.cache[key].isOverridden);
}
- overrideLocalDefault(key: string, newDefault: any) {
- // capture the previous value
- const prevDefault = this.defaults[key] ? this.defaults[key].value : undefined;
-
- // update defaults map
- this.defaults[key] = {
- ...(this.defaults[key] || {}),
- value: newDefault,
- };
-
- // update cached default value
- this.cache[key] = {
- ...(this.cache[key] || {}),
- value: newDefault,
- };
-
- // don't broadcast change if userValue was already overriding the default
- if (this.cache[key].userValue == null) {
- this.update$.next({ key, newValue: newDefault, oldValue: prevDefault });
- this.saved$.next({ key, newValue: newDefault, oldValue: prevDefault });
- }
- }
-
getUpdate$() {
return this.update$.asObservable();
}
- getSaved$() {
- return this.saved$.asObservable();
- }
-
getUpdateErrors$() {
return this.updateErrors$.asObservable();
}
@@ -178,7 +149,6 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r
try {
const { settings } = await this.api.batchSet(key, newVal);
this.cache = defaultsDeep({}, defaults, settings);
- this.saved$.next({ key, newValue: newVal, oldValue: initialVal });
return true;
} catch (error) {
this.setLocally(key, initialVal);
diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts
index 1222fc2a685de..72f03be415475 100644
--- a/src/core/public/ui_settings/ui_settings_service.mock.ts
+++ b/src/core/public/ui_settings/ui_settings_service.mock.ts
@@ -22,14 +22,11 @@ const createSetupContractMock = () => {
isDefault: jest.fn(),
isCustom: jest.fn(),
isOverridden: jest.fn(),
- overrideLocalDefault: jest.fn(),
getUpdate$: jest.fn(),
- getSaved$: jest.fn(),
getUpdateErrors$: jest.fn(),
};
setupContract.get$.mockReturnValue(new Rx.Subject());
setupContract.getUpdate$.mockReturnValue(new Rx.Subject());
- setupContract.getSaved$.mockReturnValue(new Rx.Subject());
setupContract.getUpdateErrors$.mockReturnValue(new Rx.Subject());
setupContract.getAll.mockReturnValue({});
diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts
index 4e42c960bf914..9f0c6ac5fc937 100644
--- a/src/core/public/ui_settings/ui_settings_service.test.ts
+++ b/src/core/public/ui_settings/ui_settings_service.test.ts
@@ -34,12 +34,7 @@ describe('#stop', () => {
service.stop();
await expect(
- Rx.combineLatest(
- client.getUpdate$(),
- client.getSaved$(),
- client.getUpdateErrors$(),
- loadingCount$!
- ).toPromise()
+ Rx.combineLatest(client.getUpdate$(), client.getUpdateErrors$(), loadingCount$!).toPromise()
).resolves.toBe(undefined);
});
});
diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx
index fd11314bdbd66..b897c1c73b89b 100644
--- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx
+++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx
@@ -59,7 +59,6 @@ function mockConfig() {
isCustom: (key: string) => false,
isOverridden: (key: string) => Boolean(config.getAll()[key].isOverridden),
getRegistered: () => ({} as Readonly>),
- overrideLocalDefault: (key: string, value: any) => {},
getUpdate$: () =>
new Observable<{
key: string;
diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
index 138d665866af0..44beed5e4a89b 100644
--- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap
@@ -160,14 +160,12 @@ exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = `
},
"get$": [MockFunction],
"getAll": [MockFunction],
- "getSaved$": [MockFunction],
"getUpdate$": [MockFunction],
"getUpdateErrors$": [MockFunction],
"isCustom": [MockFunction],
"isDeclared": [MockFunction],
"isDefault": [MockFunction],
"isOverridden": [MockFunction],
- "overrideLocalDefault": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
}
@@ -493,14 +491,12 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = `
},
"get$": [MockFunction],
"getAll": [MockFunction],
- "getSaved$": [MockFunction],
"getUpdate$": [MockFunction],
"getUpdateErrors$": [MockFunction],
"isCustom": [MockFunction],
"isDeclared": [MockFunction],
"isDefault": [MockFunction],
"isOverridden": [MockFunction],
- "overrideLocalDefault": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
}
@@ -840,14 +836,12 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = `
},
"get$": [MockFunction],
"getAll": [MockFunction],
- "getSaved$": [MockFunction],
"getUpdate$": [MockFunction],
"getUpdateErrors$": [MockFunction],
"isCustom": [MockFunction],
"isDeclared": [MockFunction],
"isDefault": [MockFunction],
"isOverridden": [MockFunction],
- "overrideLocalDefault": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap
index b6559114f6d2b..72fc4338684f3 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap
@@ -21,14 +21,12 @@ exports[`Header rendering renders correctly against snapshot 1`] = `
"get": [MockFunction],
"get$": [MockFunction],
"getAll": [MockFunction],
- "getSaved$": [MockFunction],
"getUpdate$": [MockFunction],
"getUpdateErrors$": [MockFunction],
"isCustom": [MockFunction],
"isDeclared": [MockFunction],
"isDefault": [MockFunction],
"isOverridden": [MockFunction],
- "overrideLocalDefault": [MockFunction],
"remove": [MockFunction],
"set": [MockFunction],
},
From 0b16688a241d3c868ba54b404861f9b71adfe792 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20St=C3=BCrmer?=
Date: Tue, 27 Apr 2021 20:06:16 +0200
Subject: [PATCH 12/70] [Logs UI] Don't break log stream on syntactically
invalid KQL (#98191)
---
.../components/log_stream/log_stream.tsx | 108 +++++++++----
.../log_stream/log_stream_embeddable.tsx | 7 +-
.../log_stream/log_stream_error_boundary.tsx | 62 ++++++++
.../components/resettable_error_boundary.tsx | 72 +++++++++
.../logs/log_filter/log_filter_state.ts | 149 +++++++++---------
.../log_filter/with_log_filter_url_state.tsx | 52 +++---
.../containers/logs/log_stream/index.ts | 31 +---
.../logs/log_summary/with_summary.ts | 5 +-
.../pages/link_to/link_to_logs.test.tsx | 22 +--
.../pages/link_to/redirect_to_logs.test.tsx | 6 +-
.../public/pages/link_to/redirect_to_logs.tsx | 2 +-
.../pages/link_to/redirect_to_node_logs.tsx | 2 +-
.../pages/logs/stream/page_providers.tsx | 24 +--
.../public/pages/logs/stream/page_toolbar.tsx | 21 +--
.../components/node_details/tabs/logs.tsx | 24 ++-
x-pack/test/functional/apps/infra/link_to.ts | 2 +-
16 files changed, 381 insertions(+), 208 deletions(-)
create mode 100644 x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx
create mode 100644 x-pack/plugins/infra/public/components/resettable_error_boundary.tsx
diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx
index a481a3897789e..5023f9d5d5fd4 100644
--- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx
+++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx
@@ -7,7 +7,7 @@
import React, { useMemo, useCallback, useEffect } from 'react';
import { noop } from 'lodash';
-import type { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
+import { DataPublicPluginStart, esQuery, Filter } from '../../../../../../src/plugins/data/public';
import { euiStyled } from '../../../../../../src/plugins/kibana_react/common';
import { LogEntryCursor } from '../../../common/log_entry';
@@ -19,6 +19,7 @@ import { ScrollableLogTextStreamView } from '../logging/log_text_stream';
import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration';
import { JsonValue } from '../../../../../../src/plugins/kibana_utils/common';
import { Query } from '../../../../../../src/plugins/data/common';
+import { LogStreamErrorBoundary } from './log_stream_error_boundary';
interface LogStreamPluginDeps {
data: DataPublicPluginStart;
@@ -57,25 +58,39 @@ type LogColumnDefinition =
| MessageColumnDefinition
| FieldColumnDefinition;
-export interface LogStreamProps {
+export interface LogStreamProps extends LogStreamContentProps {
+ height?: string | number;
+}
+
+interface LogStreamContentProps {
sourceId?: string;
startTimestamp: number;
endTimestamp: number;
query?: string | Query | BuiltEsQuery;
+ filters?: Filter[];
center?: LogEntryCursor;
highlight?: string;
- height?: string | number;
columns?: LogColumnDefinition[];
}
-export const LogStream: React.FC = ({
+export const LogStream: React.FC = ({ height = 400, ...contentProps }) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const LogStreamContent: React.FC = ({
sourceId = 'default',
startTimestamp,
endTimestamp,
query,
+ filters,
center,
highlight,
- height = '400px',
columns,
}) => {
const customColumns = useMemo(
@@ -99,12 +114,21 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
sourceConfiguration,
loadSourceConfiguration,
isLoadingSourceConfiguration,
+ derivedIndexPattern,
} = useLogSource({
sourceId,
fetch: services.http.fetch,
indexPatternsService: services.data.indexPatterns,
});
+ const parsedQuery = useMemo(() => {
+ if (typeof query === 'object' && 'bool' in query) {
+ return mergeBoolQueries(query, esQuery.buildEsQuery(derivedIndexPattern, [], filters ?? []));
+ } else {
+ return esQuery.buildEsQuery(derivedIndexPattern, coerceToQueries(query), filters ?? []);
+ }
+ }, [derivedIndexPattern, filters, query]);
+
// Internal state
const {
entries,
@@ -119,7 +143,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
sourceId,
startTimestamp,
endTimestamp,
- query,
+ query: parsedQuery,
center,
columns: customColumns,
});
@@ -138,8 +162,6 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
[entries]
);
- const parsedHeight = typeof height === 'number' ? `${height}px` : height;
-
// Component lifetime
useEffect(() => {
loadSourceConfiguration();
@@ -170,37 +192,34 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
);
return (
-
-
-
+
);
};
-const LogStreamContent = euiStyled.div<{ height: string }>`
+const LogStreamContainer = euiStyled.div`
display: flex;
background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
- height: ${(props) => props.height};
`;
function convertLogColumnDefinitionToLogSourceColumnDefinition(
@@ -227,6 +246,27 @@ function convertLogColumnDefinitionToLogSourceColumnDefinition(
});
}
+const mergeBoolQueries = (firstQuery: BuiltEsQuery, secondQuery: BuiltEsQuery): BuiltEsQuery => ({
+ bool: {
+ must: [...firstQuery.bool.must, ...secondQuery.bool.must],
+ filter: [...firstQuery.bool.filter, ...secondQuery.bool.filter],
+ should: [...firstQuery.bool.should, ...secondQuery.bool.should],
+ must_not: [...firstQuery.bool.must_not, ...secondQuery.bool.must_not],
+ },
+});
+
+const coerceToQueries = (value: undefined | string | Query): Query[] => {
+ if (value == null) {
+ return [];
+ } else if (typeof value === 'string') {
+ return [{ language: 'kuery', query: value }];
+ } else if ('language' in value && 'query' in value) {
+ return [value];
+ }
+
+ return [];
+};
+
// Allow for lazy loading
// eslint-disable-next-line import/no-default-export
export default LogStream;
diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx
index 1d9edd7289236..e3fc4ca1de565 100644
--- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx
+++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx
@@ -9,7 +9,7 @@ import { CoreStart } from 'kibana/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
-import { Query, TimeRange, esQuery, Filter } from '../../../../../../src/plugins/data/public';
+import { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/public';
import {
Embeddable,
EmbeddableInput,
@@ -69,8 +69,6 @@ export class LogStreamEmbeddable extends Embeddable {
return;
}
- const parsedQuery = esQuery.buildEsQuery(undefined, this.input.query, this.input.filters);
-
const startTimestamp = datemathToEpochMillis(this.input.timeRange.from);
const endTimestamp = datemathToEpochMillis(this.input.timeRange.to, 'up');
@@ -86,7 +84,8 @@ export class LogStreamEmbeddable extends Embeddable {
startTimestamp={startTimestamp}
endTimestamp={endTimestamp}
height="100%"
- query={parsedQuery}
+ query={this.input.query}
+ filters={this.input.filters}
/>
diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx
new file mode 100644
index 0000000000000..c55e6d299127b
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_error_boundary.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { KQLSyntaxError } from '../../../../../../src/plugins/data/common';
+import { RenderErrorFunc, ResettableErrorBoundary } from '../resettable_error_boundary';
+
+export const LogStreamErrorBoundary: React.FC<{ resetOnChange: any }> = ({
+ children,
+ resetOnChange = null,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const LogStreamErrorContent: React.FC<{
+ error: any;
+}> = ({ error }) => {
+ if (error instanceof KQLSyntaxError) {
+ return (
+
+ }
+ body={{error.message} }
+ />
+ );
+ } else {
+ return (
+
+ }
+ body={{error.message} }
+ />
+ );
+ }
+};
+
+const renderLogStreamErrorContent: RenderErrorFunc = ({ latestError }) => (
+
+);
diff --git a/x-pack/plugins/infra/public/components/resettable_error_boundary.tsx b/x-pack/plugins/infra/public/components/resettable_error_boundary.tsx
new file mode 100644
index 0000000000000..6e9dc178a4d84
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/resettable_error_boundary.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import equal from 'fast-deep-equal';
+import React from 'react';
+
+export interface RenderErrorFuncArgs {
+ latestError: any;
+ resetError: () => void;
+}
+
+export type RenderErrorFunc = (renderErrorArgs: RenderErrorFuncArgs) => React.ReactNode;
+
+interface ResettableErrorBoundaryProps {
+ renderError: RenderErrorFunc;
+ resetOnChange: ResetOnChange;
+}
+
+interface ResettableErrorBoundaryState {
+ latestError: any;
+}
+
+export class ResettableErrorBoundary extends React.Component<
+ ResettableErrorBoundaryProps,
+ ResettableErrorBoundaryState
+> {
+ state: ResettableErrorBoundaryState = {
+ latestError: undefined,
+ };
+
+ componentDidUpdate({
+ resetOnChange: prevResetOnChange,
+ }: ResettableErrorBoundaryProps) {
+ const { resetOnChange } = this.props;
+ const { latestError } = this.state;
+
+ if (latestError != null && !equal(resetOnChange, prevResetOnChange)) {
+ this.resetError();
+ }
+ }
+
+ static getDerivedStateFromError(error: any) {
+ return {
+ latestError: error,
+ };
+ }
+
+ render() {
+ const { children, renderError } = this.props;
+ const { latestError } = this.state;
+
+ if (latestError != null) {
+ return renderError({
+ latestError,
+ resetError: this.resetError,
+ });
+ }
+
+ return children;
+ }
+
+ resetError = () => {
+ this.setState((previousState) => ({
+ ...previousState,
+ latestError: undefined,
+ }));
+ };
+}
diff --git a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts
index 5eece62c683e1..6a78d7c6f94bc 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts
@@ -5,95 +5,100 @@
* 2.0.
*/
-import { useState, useMemo } from 'react';
import createContainer from 'constate';
-import { IIndexPattern } from 'src/plugins/data/public';
-import { esKuery } from '../../../../../../../src/plugins/data/public';
-import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
+import { useCallback, useState } from 'react';
+import { useDebounce } from 'react-use';
+import { esQuery, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public';
-export interface KueryFilterQuery {
- kind: 'kuery';
- expression: string;
-}
-
-export interface SerializedFilterQuery {
- query: KueryFilterQuery;
- serializedQuery: string;
-}
+type ParsedQuery = ReturnType;
-interface LogFilterInternalStateParams {
- filterQuery: SerializedFilterQuery | null;
- filterQueryDraft: KueryFilterQuery | null;
+interface ILogFilterState {
+ filterQuery: {
+ parsedQuery: ParsedQuery;
+ serializedQuery: string;
+ originalQuery: Query;
+ } | null;
+ filterQueryDraft: Query;
+ validationErrors: string[];
}
-export const logFilterInitialState: LogFilterInternalStateParams = {
+const initialLogFilterState: ILogFilterState = {
filterQuery: null,
- filterQueryDraft: null,
+ filterQueryDraft: {
+ language: 'kuery',
+ query: '',
+ },
+ validationErrors: [],
};
-export type LogFilterStateParams = Omit & {
- filterQuery: SerializedFilterQuery['serializedQuery'] | null;
- filterQueryAsKuery: SerializedFilterQuery['query'] | null;
- isFilterQueryDraftValid: boolean;
-};
-export interface LogFilterCallbacks {
- setLogFilterQueryDraft: (expression: string) => void;
- applyLogFilterQuery: (expression: string) => void;
-}
+const validationDebounceTimeout = 1000; // milliseconds
-export const useLogFilterState: (props: {
- indexPattern: IIndexPattern;
-}) => LogFilterStateParams & LogFilterCallbacks = ({ indexPattern }) => {
- const [state, setState] = useState(logFilterInitialState);
- const { filterQuery, filterQueryDraft } = state;
+export const useLogFilterState = ({ indexPattern }: { indexPattern: IIndexPattern }) => {
+ const [logFilterState, setLogFilterState] = useState(initialLogFilterState);
- const setLogFilterQueryDraft = useMemo(() => {
- const setDraft = (payload: KueryFilterQuery) =>
- setState((prevState) => ({ ...prevState, filterQueryDraft: payload }));
- return (expression: string) =>
- setDraft({
- kind: 'kuery',
- expression,
- });
+ const parseQuery = useCallback(
+ (filterQuery: Query) => esQuery.buildEsQuery(indexPattern, filterQuery, []),
+ [indexPattern]
+ );
+
+ const setLogFilterQueryDraft = useCallback((filterQueryDraft: Query) => {
+ setLogFilterState((previousLogFilterState) => ({
+ ...previousLogFilterState,
+ filterQueryDraft,
+ validationErrors: [],
+ }));
}, []);
- const applyLogFilterQuery = useMemo(() => {
- const applyQuery = (payload: SerializedFilterQuery) =>
- setState((prevState) => ({
- ...prevState,
- filterQueryDraft: payload.query,
- filterQuery: payload,
- }));
- return (expression: string) =>
- applyQuery({
- query: {
- kind: 'kuery',
- expression,
- },
- serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern),
+
+ const [, cancelPendingValidation] = useDebounce(
+ () => {
+ setLogFilterState((previousLogFilterState) => {
+ try {
+ parseQuery(logFilterState.filterQueryDraft);
+ return {
+ ...previousLogFilterState,
+ validationErrors: [],
+ };
+ } catch (error) {
+ return {
+ ...previousLogFilterState,
+ validationErrors: [`${error}`],
+ };
+ }
});
- }, [indexPattern]);
+ },
+ validationDebounceTimeout,
+ [logFilterState.filterQueryDraft, parseQuery]
+ );
- const isFilterQueryDraftValid = useMemo(() => {
- if (filterQueryDraft?.kind === 'kuery') {
+ const applyLogFilterQuery = useCallback(
+ (filterQuery: Query) => {
+ cancelPendingValidation();
try {
- esKuery.fromKueryExpression(filterQueryDraft.expression);
- } catch (err) {
- return false;
+ const parsedQuery = parseQuery(filterQuery);
+ setLogFilterState((previousLogFilterState) => ({
+ ...previousLogFilterState,
+ filterQuery: {
+ parsedQuery,
+ serializedQuery: JSON.stringify(parsedQuery),
+ originalQuery: filterQuery,
+ },
+ filterQueryDraft: filterQuery,
+ validationErrors: [],
+ }));
+ } catch (error) {
+ setLogFilterState((previousLogFilterState) => ({
+ ...previousLogFilterState,
+ validationErrors: [`${error}`],
+ }));
}
- }
-
- return true;
- }, [filterQueryDraft]);
-
- const serializedFilterQuery = useMemo(() => (filterQuery ? filterQuery.serializedQuery : null), [
- filterQuery,
- ]);
+ },
+ [cancelPendingValidation, parseQuery]
+ );
return {
- ...state,
- filterQueryAsKuery: state.filterQuery ? state.filterQuery.query : null,
- filterQuery: serializedFilterQuery,
- isFilterQueryDraftValid,
+ filterQuery: logFilterState.filterQuery,
+ filterQueryDraft: logFilterState.filterQueryDraft,
+ isFilterQueryDraftValid: logFilterState.validationErrors.length === 0,
setLogFilterQueryDraft,
applyLogFilterQuery,
};
diff --git a/x-pack/plugins/infra/public/containers/logs/log_filter/with_log_filter_url_state.tsx b/x-pack/plugins/infra/public/containers/logs/log_filter/with_log_filter_url_state.tsx
index 6bc71fc880434..f085a2c7d275b 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_filter/with_log_filter_url_state.tsx
+++ b/x-pack/plugins/infra/public/containers/logs/log_filter/with_log_filter_url_state.tsx
@@ -5,43 +5,57 @@
* 2.0.
*/
+import * as rt from 'io-ts';
import React, { useContext } from 'react';
-import { LogFilterState, LogFilterStateParams } from './log_filter_state';
+import { Query } from '../../../../../../../src/plugins/data/public';
import { replaceStateKeyInQueryString, UrlStateContainer } from '../../../utils/url_state';
-
-type LogFilterUrlState = LogFilterStateParams['filterQueryAsKuery'];
+import { LogFilterState } from './log_filter_state';
export const WithLogFilterUrlState: React.FC = () => {
- const { filterQueryAsKuery, applyLogFilterQuery } = useContext(LogFilterState.Context);
+ const { filterQuery, applyLogFilterQuery } = useContext(LogFilterState.Context);
+
return (
{
if (urlState) {
- applyLogFilterQuery(urlState.expression);
+ applyLogFilterQuery(urlState);
}
}}
onInitialize={(urlState) => {
if (urlState) {
- applyLogFilterQuery(urlState.expression);
+ applyLogFilterQuery(urlState);
}
}}
/>
);
};
-const mapToFilterQuery = (value: any): LogFilterUrlState | undefined =>
- value?.kind === 'kuery' && typeof value.expression === 'string'
- ? {
- kind: value.kind,
- expression: value.expression,
- }
- : undefined;
+const mapToFilterQuery = (value: any): Query | undefined => {
+ if (legacyFilterQueryUrlStateRT.is(value)) {
+ // migrate old url state
+ return {
+ language: value.kind,
+ query: value.expression,
+ };
+ } else if (filterQueryUrlStateRT.is(value)) {
+ return value;
+ } else {
+ return undefined;
+ }
+};
+
+export const replaceLogFilterInQueryString = (query: Query) =>
+ replaceStateKeyInQueryString('logFilter', query);
+
+const filterQueryUrlStateRT = rt.type({
+ language: rt.string,
+ query: rt.string,
+});
-export const replaceLogFilterInQueryString = (expression: string) =>
- replaceStateKeyInQueryString('logFilter', {
- kind: 'kuery',
- expression,
- });
+const legacyFilterQueryUrlStateRT = rt.type({
+ kind: rt.literal('kuery'),
+ expression: rt.string,
+});
diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
index cd68048e6c94f..021aa8f79fe59 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import { useState, useCallback, useEffect, useMemo } from 'react';
import createContainer from 'constate';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import useSetState from 'react-use/lib/useSetState';
-import { esKuery, esQuery, Query } from '../../../../../../../src/plugins/data/public';
+import { esQuery } from '../../../../../../../src/plugins/data/public';
import { LogEntry, LogEntryCursor } from '../../../../common/log_entry';
import { useSubscription } from '../../../utils/use_observable';
import { LogSourceConfigurationProperties } from '../log_source';
@@ -23,7 +23,7 @@ interface LogStreamProps {
sourceId: string;
startTimestamp: number;
endTimestamp: number;
- query?: string | Query | BuiltEsQuery;
+ query?: BuiltEsQuery;
center?: LogEntryCursor;
columns?: LogSourceConfigurationProperties['logColumns'];
}
@@ -77,27 +77,15 @@ export function useLogStream({
}
}, [prevEndTimestamp, endTimestamp, setState]);
- const parsedQuery = useMemo(() => {
- if (!query) {
- return undefined;
- } else if (typeof query === 'string') {
- return esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query));
- } else if ('language' in query) {
- return getEsQueryFromQueryObject(query);
- } else {
- return query;
- }
- }, [query]);
-
const commonFetchArguments = useMemo(
() => ({
sourceId,
startTimestamp,
endTimestamp,
- query: parsedQuery,
+ query,
columnOverrides: columns,
}),
- [columns, endTimestamp, parsedQuery, sourceId, startTimestamp]
+ [columns, endTimestamp, query, sourceId, startTimestamp]
);
const {
@@ -268,13 +256,4 @@ export function useLogStream({
};
}
-function getEsQueryFromQueryObject(query: Query) {
- switch (query.language) {
- case 'kuery':
- return esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query.query as string));
- case 'lucene':
- return esQuery.luceneStringToDsl(query.query as string);
- }
-}
-
export const [LogStreamProvider, useLogStreamContext] = createContainer(useLogStream);
diff --git a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts
index cbd664729d5c2..9204c81816e83 100644
--- a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts
+++ b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts
@@ -7,12 +7,11 @@
import { useContext } from 'react';
import useThrottle from 'react-use/lib/useThrottle';
-
import { RendererFunction } from '../../../utils/typed_react';
-import { LogSummaryBuckets, useLogSummary } from './log_summary';
import { LogFilterState } from '../log_filter';
import { LogPositionState } from '../log_position';
import { useLogSourceContext } from '../log_source';
+import { LogSummaryBuckets, useLogSummary } from './log_summary';
const FETCH_THROTTLE_INTERVAL = 3000;
@@ -37,7 +36,7 @@ export const WithSummary = ({
sourceId,
throttledStartTimestamp,
throttledEndTimestamp,
- filterQuery
+ filterQuery?.serializedQuery ?? null
);
return children({ buckets, start, end });
diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
index 0d57f8dad1e72..91f42509d493a 100644
--- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
+++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx
@@ -66,7 +66,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"`
+ `"(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"`
);
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
@@ -86,7 +86,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE');
- expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(expression:'',kind:kuery)"`);
+ expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(language:kuery,query:'')"`);
expect(searchParams.get('logPosition')).toEqual(null);
});
});
@@ -106,7 +106,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)"`
+ `"(language:kuery,query:'FILTER_FIELD:FILTER_VALUE')"`
);
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
@@ -126,7 +126,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE');
- expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(expression:'',kind:kuery)"`);
+ expect(searchParams.get('logFilter')).toMatchInlineSnapshot(`"(language:kuery,query:'')"`);
expect(searchParams.get('logPosition')).toEqual(null);
});
});
@@ -146,7 +146,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'HOST_FIELD: HOST_NAME',kind:kuery)"`
+ `"(language:kuery,query:'HOST_FIELD: HOST_NAME')"`
);
expect(searchParams.get('logPosition')).toEqual(null);
});
@@ -167,7 +167,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"`
+ `"(language:kuery,query:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"`
);
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
@@ -188,7 +188,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'HOST_FIELD: HOST_NAME',kind:kuery)"`
+ `"(language:kuery,query:'HOST_FIELD: HOST_NAME')"`
);
expect(searchParams.get('logPosition')).toEqual(null);
});
@@ -223,7 +223,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'CONTAINER_FIELD: CONTAINER_ID',kind:kuery)"`
+ `"(language:kuery,query:'CONTAINER_FIELD: CONTAINER_ID')"`
);
expect(searchParams.get('logPosition')).toEqual(null);
});
@@ -244,7 +244,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"`
+ `"(language:kuery,query:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"`
);
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
@@ -281,7 +281,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'POD_FIELD: POD_UID',kind:kuery)"`
+ `"(language:kuery,query:'POD_FIELD: POD_UID')"`
);
expect(searchParams.get('logPosition')).toEqual(null);
});
@@ -300,7 +300,7 @@ describe('LinkToLogsPage component', () => {
const searchParams = new URLSearchParams(history.location.search);
expect(searchParams.get('sourceId')).toEqual('default');
expect(searchParams.get('logFilter')).toMatchInlineSnapshot(
- `"(expression:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)',kind:kuery)"`
+ `"(language:kuery,query:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"`
);
expect(searchParams.get('logPosition')).toMatchInlineSnapshot(
`"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"`
diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx
index a3e261a6bc280..39f276b982d76 100644
--- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx
+++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx
@@ -20,7 +20,7 @@ describe('RedirectToLogs component', () => {
expect(component).toMatchInlineSnapshot(`
`);
});
@@ -34,7 +34,7 @@ describe('RedirectToLogs component', () => {
expect(component).toMatchInlineSnapshot(`
`);
});
@@ -46,7 +46,7 @@ describe('RedirectToLogs component', () => {
expect(component).toMatchInlineSnapshot(`
`);
});
diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx
index 9606f343dbfdf..4d77077c19a99 100644
--- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx
+++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_logs.tsx
@@ -26,7 +26,7 @@ export const RedirectToLogs = ({ location, match }: RedirectToLogsProps) => {
const sourceId = match.params.sourceId || 'default';
const filter = getFilterFromLocation(location);
const searchString = flowRight(
- replaceLogFilterInQueryString(filter),
+ replaceLogFilterInQueryString({ language: 'kuery', query: filter }),
replaceLogPositionInQueryString(getTimeFromLocation(location)),
replaceSourceIdInQueryString(sourceId)
)('');
diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx
index 741fad5a5310e..0df8e639b149b 100644
--- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx
+++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx
@@ -68,7 +68,7 @@ export const RedirectToNodeLogs = ({
const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter;
const searchString = flowRight(
- replaceLogFilterInQueryString(filter),
+ replaceLogFilterInQueryString({ language: 'kuery', query: filter }),
replaceLogPositionInQueryString(getTimeFromLocation(location)),
replaceSourceIdInQueryString(sourceId)
)('');
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx
index d987cbeb439cc..27235295013e3 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx
@@ -50,28 +50,30 @@ const LogEntriesStateProvider: React.FC = ({ children }) => {
const { startTimestamp, endTimestamp, targetPosition, isInitialized } = useContext(
LogPositionState.Context
);
- const { filterQueryAsKuery } = useContext(LogFilterState.Context);
+ const { filterQuery } = useContext(LogFilterState.Context);
// Don't render anything if the date range is incorrect.
if (!startTimestamp || !endTimestamp) {
return null;
}
- const logStreamProps = {
- sourceId,
- startTimestamp,
- endTimestamp,
- query: filterQueryAsKuery?.expression ?? undefined,
- center: targetPosition ?? undefined,
- };
-
// Don't initialize the entries until the position has been fully intialized.
// See ` `
if (!isInitialized) {
return null;
}
- return {children} ;
+ return (
+
+ {children}
+
+ );
};
const LogHighlightsStateProvider: React.FC = ({ children }) => {
@@ -86,7 +88,7 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => {
entriesEnd: bottomCursor,
centerCursor: entries.length > 0 ? entries[Math.floor(entries.length / 2)].cursor : null,
size: entries.length,
- filterQuery,
+ filterQuery: filterQuery?.serializedQuery ?? null,
};
return {children} ;
};
diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx
index 971eb1b3e486f..fc37cd2e11c1b 100644
--- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx
+++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx
@@ -62,25 +62,18 @@ export const LogsToolbar = () => {
iconType="search"
indexPatterns={[derivedIndexPattern]}
isInvalid={!isFilterQueryDraftValid}
- onChange={(expression: Query) => {
- if (typeof expression.query === 'string') {
- setSurroundingLogsId(null);
- setLogFilterQueryDraft(expression.query);
- }
+ onChange={(query: Query) => {
+ setSurroundingLogsId(null);
+ setLogFilterQueryDraft(query);
}}
- onSubmit={(expression: Query) => {
- if (typeof expression.query === 'string') {
- setSurroundingLogsId(null);
- applyLogFilterQuery(expression.query);
- }
+ onSubmit={(query: Query) => {
+ setSurroundingLogsId(null);
+ applyLogFilterQuery(query);
}}
placeholder={i18n.translate('xpack.infra.logsPage.toolbar.kqlSearchFieldPlaceholder', {
defaultMessage: 'Search for log entries… (e.g. host.name:host-1)',
})}
- query={{
- query: filterQueryDraft?.expression ?? '',
- language: filterQueryDraft?.kind ?? 'kuery',
- }}
+ query={filterQueryDraft}
/>
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx
index 71bf9e50c4bb6..4fa9fdf8cdd4a 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx
@@ -6,6 +6,7 @@
*/
import React, { useCallback, useMemo, useState } from 'react';
+import { useThrottle } from 'react-use';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiFieldSearch } from '@elastic/eui';
@@ -26,16 +27,21 @@ const TabComponent = (props: TabProps) => {
const { nodeType } = useWaffleOptionsContext();
const { options, node } = props;
+ const throttledTextQuery = useThrottle(textQuery, textQueryThrottleInterval);
+
const filter = useMemo(() => {
- let query = options.fields
- ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`
- : ``;
+ const query = [
+ ...(options.fields != null
+ ? [`${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`]
+ : []),
+ ...(throttledTextQuery !== '' ? [throttledTextQuery] : []),
+ ].join(' and ');
- if (textQuery) {
- query += ` and message: ${textQuery}`;
- }
- return query;
- }, [options, nodeType, node.id, textQuery]);
+ return {
+ language: 'kuery',
+ query,
+ };
+ }, [options.fields, nodeType, node.id, throttledTextQuery]);
const onQueryChange = useCallback((e: React.ChangeEvent) => {
setTextQuery(e.target.value);
@@ -89,3 +95,5 @@ export const LogsTab = {
}),
content: TabComponent,
};
+
+const textQueryThrottleInterval = 1000; // milliseconds
diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts
index 3e070fb9849b1..ebfcb740961b1 100644
--- a/x-pack/test/functional/apps/infra/link_to.ts
+++ b/x-pack/test/functional/apps/infra/link_to.ts
@@ -45,7 +45,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(parsedUrl.pathname).to.be('/app/logs/stream');
expect(parsedUrl.searchParams.get('logFilter')).to.be(
- `(expression:'trace.id:${traceId}',kind:kuery)`
+ `(language:kuery,query:'trace.id:${traceId}')`
);
expect(parsedUrl.searchParams.get('logPosition')).to.be(
`(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)`
From 31660419eafdd8c74c68123745fc699e7ee8cf2e Mon Sep 17 00:00:00 2001
From: Spencer
Date: Tue, 27 Apr 2021 11:31:07 -0700
Subject: [PATCH 13/70] [kbn/es] disable geoip downloader at the root (#98372)
Co-authored-by: spalger
---
packages/kbn-es/src/cluster.js | 7 +++++--
packages/kbn-es/src/integration_tests/cluster.test.js | 2 ++
test/common/config.js | 2 +-
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js
index 236d5cf252136..c55e5d3513c44 100644
--- a/packages/kbn-es/src/cluster.js
+++ b/packages/kbn-es/src/cluster.js
@@ -246,7 +246,10 @@ exports.Cluster = class Cluster {
this._log.info(chalk.bold('Starting'));
this._log.indent(4);
- const esArgs = ['action.destructive_requires_name=true'].concat(options.esArgs || []);
+ const esArgs = [
+ 'action.destructive_requires_name=true',
+ 'ingest.geoip.downloader.enabled=false',
+ ].concat(options.esArgs || []);
// Add to esArgs if ssl is enabled
if (this._ssl) {
@@ -272,7 +275,7 @@ exports.Cluster = class Cluster {
// especially because we currently run many instances of ES on the same machine during CI
options.esEnvVars.ES_JAVA_OPTS =
(options.esEnvVars.ES_JAVA_OPTS ? `${options.esEnvVars.ES_JAVA_OPTS} ` : '') +
- '-Xms2g -Xmx2g';
+ '-Xms1g -Xmx1g';
this._process = execa(ES_BIN, args, {
cwd: installPath,
diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js
index 9a9ffb2afc331..6b4025840283f 100644
--- a/packages/kbn-es/src/integration_tests/cluster.test.js
+++ b/packages/kbn-es/src/integration_tests/cluster.test.js
@@ -266,6 +266,7 @@ describe('#start(installPath)', () => {
Array [
Array [
"action.destructive_requires_name=true",
+ "ingest.geoip.downloader.enabled=false",
],
undefined,
Object {
@@ -344,6 +345,7 @@ describe('#run()', () => {
Array [
Array [
"action.destructive_requires_name=true",
+ "ingest.geoip.downloader.enabled=false",
],
undefined,
Object {
diff --git a/test/common/config.js b/test/common/config.js
index b44f2de5042eb..84848347f94cd 100644
--- a/test/common/config.js
+++ b/test/common/config.js
@@ -21,7 +21,7 @@ export default function () {
servers,
esTestCluster: {
- serverArgs: ['xpack.security.enabled=false', 'geoip.downloader.enabled=false'],
+ serverArgs: ['xpack.security.enabled=false'],
},
kbnTestServer: {
From a386fd86f0b0210e967b031e1d33d9ab3cf11c58 Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Tue, 27 Apr 2021 21:00:44 +0200
Subject: [PATCH 14/70] Fix SO migration integration tests (#98478)
* do not restart Kibana server on integration tests writing logs
* unskip tests
* do not write to ended stream to avoid a race condition
* revert changes to the File appender
* fix race condition on the logging_system level
enforce buffer usage for all the logs created during disposal phase
---
.../src/get_server_watch_paths.test.ts | 4 ++
.../src/get_server_watch_paths.ts | 1 +
.../server/logging/logging_system.test.ts | 56 +++++++++++++++++++
src/core/server/logging/logging_system.ts | 32 +++++++----
.../integration_tests/cleanup.test.ts | 3 +-
.../integration_tests/rewriting_id.test.ts | 3 +-
6 files changed, 84 insertions(+), 15 deletions(-)
diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts
index ff25f2a7bf55e..2fd53dd83a1bd 100644
--- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts
+++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts
@@ -42,21 +42,25 @@ it('produces the right watch and ignore list', () => {
/\\\\\\.\\(md\\|sh\\|txt\\)\\$/,
/debug\\\\\\.log\\$/,
/src/plugins/*/test/**,
+ /src/plugins/*/integration_tests/**,
/src/plugins/*/build/**,
/src/plugins/*/target/**,
/src/plugins/*/scripts/**,
/src/plugins/*/docs/**,
/test/plugin_functional/plugins/*/test/**,
+ /test/plugin_functional/plugins/*/integration_tests/**,
/test/plugin_functional/plugins/*/build/**,
/test/plugin_functional/plugins/*/target/**,
/test/plugin_functional/plugins/*/scripts/**,
/test/plugin_functional/plugins/*/docs/**,
/x-pack/plugins/*/test/**,
+ /x-pack/plugins/*/integration_tests/**,
/x-pack/plugins/*/build/**,
/x-pack/plugins/*/target/**,
/x-pack/plugins/*/scripts/**,
/x-pack/plugins/*/docs/**,
/x-pack/test/plugin_functional/plugins/resolver_test/test/**,
+ /x-pack/test/plugin_functional/plugins/resolver_test/integration_tests/**,
/x-pack/test/plugin_functional/plugins/resolver_test/build/**,
/x-pack/test/plugin_functional/plugins/resolver_test/target/**,
/x-pack/test/plugin_functional/plugins/resolver_test/scripts/**,
diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts
index 53aa53b5aa63a..4a9dae5c6fee2 100644
--- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts
+++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts
@@ -28,6 +28,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) {
(acc: string[], path) => [
...acc,
Path.resolve(path, 'test/**'),
+ Path.resolve(path, 'integration_tests/**'),
Path.resolve(path, 'build/**'),
Path.resolve(path, 'target/**'),
Path.resolve(path, 'scripts/**'),
diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts
index 9c4313bc0c49d..8eed2aecb21d6 100644
--- a/src/core/server/logging/logging_system.test.ts
+++ b/src/core/server/logging/logging_system.test.ts
@@ -470,3 +470,59 @@ test('subsequent calls to setContextConfig() for the same context name can disab
},
});
});
+
+test('buffers log records for already created appenders', async () => {
+ // a default config
+ await system.upgrade(
+ config.schema.validate({
+ appenders: { default: { type: 'console', layout: { type: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ const logger = system.get('test', 'context');
+
+ const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append');
+
+ const upgradePromise = system.upgrade(
+ config.schema.validate({
+ appenders: { default: { type: 'console', layout: { type: 'json' } } },
+ root: { level: 'all' },
+ })
+ );
+
+ logger.trace('message to the known context');
+ expect(bufferAppendSpy).toHaveBeenCalledTimes(1);
+ expect(mockConsoleLog).toHaveBeenCalledTimes(0);
+
+ await upgradePromise;
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0]).message).toBe('message to the known context');
+});
+
+test('buffers log records for appenders created during config upgrade', async () => {
+ // a default config
+ await system.upgrade(
+ config.schema.validate({
+ appenders: { default: { type: 'console', layout: { type: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append');
+
+ const upgradePromise = system.upgrade(
+ config.schema.validate({
+ appenders: { default: { type: 'console', layout: { type: 'json' } } },
+ root: { level: 'all' },
+ })
+ );
+
+ const logger = system.get('test', 'context');
+ logger.trace('message to a new context');
+
+ expect(bufferAppendSpy).toHaveBeenCalledTimes(1);
+ expect(mockConsoleLog).toHaveBeenCalledTimes(0);
+
+ await upgradePromise;
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0]).message).toBe('message to a new context');
+});
diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts
index d7c34b48c4101..45a687493c163 100644
--- a/src/core/server/logging/logging_system.ts
+++ b/src/core/server/logging/logging_system.ts
@@ -167,17 +167,13 @@ export class LoggingSystem implements LoggerFactory {
}
private async applyBaseConfig(newBaseConfig: LoggingConfig) {
+ this.enforceBufferAppendersUsage();
+
const computedConfig = [...this.contextConfigs.values()].reduce(
(baseConfig, contextConfig) => baseConfig.extend(contextConfig),
newBaseConfig
);
- // reconfigure all the loggers without configuration to have them use the buffer
- // appender while we are awaiting for the appenders to be disposed.
- for (const [loggerKey, loggerAdapter] of this.loggers) {
- loggerAdapter.updateLogger(this.createLogger(loggerKey, undefined));
- }
-
// Appenders must be reset, so we first dispose of the current ones, then
// build up a new set of appenders.
await Promise.all([...this.appenders.values()].map((a) => a.dispose()));
@@ -204,18 +200,32 @@ export class LoggingSystem implements LoggerFactory {
}
}
- for (const [loggerKey, loggerAdapter] of this.loggers) {
- loggerAdapter.updateLogger(this.createLogger(loggerKey, computedConfig));
- }
-
+ this.enforceConfiguredAppendersUsage(computedConfig);
// We keep a reference to the base config so we can properly extend it
// on each config change.
this.baseConfig = newBaseConfig;
- this.computedConfig = computedConfig;
// Re-log all buffered log records with newly configured appenders.
for (const logRecord of this.bufferAppender.flush()) {
this.get(logRecord.context).log(logRecord);
}
}
+
+ // reconfigure all the loggers to have them use the buffer appender
+ // while we are awaiting for the appenders to be disposed.
+ private enforceBufferAppendersUsage() {
+ for (const [loggerKey, loggerAdapter] of this.loggers) {
+ loggerAdapter.updateLogger(this.createLogger(loggerKey, undefined));
+ }
+
+ // new loggers created during applyBaseConfig execution should use the buffer appender as well
+ this.computedConfig = undefined;
+ }
+
+ private enforceConfiguredAppendersUsage(config: LoggingConfig) {
+ for (const [loggerKey, loggerAdapter] of this.loggers) {
+ loggerAdapter.updateLogger(this.createLogger(loggerKey, config));
+ }
+ this.computedConfig = config;
+ }
}
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts
index 997105587da68..48bb282da18f6 100644
--- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts
@@ -53,8 +53,7 @@ function createRoot() {
);
}
-// CI FAILURE: https://github.com/elastic/kibana/issues/98352
-describe.skip('migration v2', () => {
+describe('migration v2', () => {
let esServer: kbnTestServer.TestElasticsearchUtils;
let root: Root;
diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts
index ed6a448b115d0..9f7e32c49ef15 100644
--- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts
@@ -88,8 +88,7 @@ function createRoot() {
);
}
-// CI FAILURE: https://github.com/elastic/kibana/issues/98351
-describe.skip('migration v2', () => {
+describe('migration v2', () => {
let esServer: kbnTestServer.TestElasticsearchUtils;
let root: Root;
From 6ed93c3571b4a655d8545397497f8603573910e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Tue, 27 Apr 2021 21:45:19 +0200
Subject: [PATCH 15/70] [Telemetry] Remove all usages of `any` (#98338)
---
.eslintrc.js | 18 +
api_docs/telemetry.json | 890 +++++++++++++++---
api_docs/telemetry.mdx | 9 +-
...na-plugin-plugins-data-public.searchbar.md | 4 +-
packages/kbn-analytics/src/reporter.ts | 2 +-
packages/kbn-analytics/src/storage.ts | 6 +-
packages/kbn-analytics/src/util.ts | 2 +-
.../public/management_app/types.ts | 3 +-
src/plugins/data/server/server.api.md | 1 -
...emetry_application_usage_collector.test.ts | 19 +-
.../register_config_usage_collector.test.ts | 2 +-
.../core/core_usage_collector.test.ts | 2 +-
.../kibana/get_saved_object_counts.test.ts | 6 +-
.../server/collectors/kibana/index.test.ts | 3 +-
.../telemetry_management_collector.test.ts | 7 +-
.../telemetry_management_collector.ts | 5 +-
.../server/collectors/ui_metric/index.test.ts | 56 +-
.../get_telemetry_failure_details.test.ts | 12 +-
.../get_telemetry_opt_in.test.ts | 3 +-
.../components/opted_in_notice_banner.tsx | 2 +-
src/plugins/telemetry/public/index.ts | 3 +-
.../render_opt_in_banner.tsx | 2 +-
.../public/services/telemetry_sender.ts | 4 +-
.../collectors/usage/ensure_deep_object.ts | 5 +-
.../usage/telemetry_usage_collector.test.ts | 12 +-
src/plugins/telemetry/server/fetcher.ts | 19 +-
src/plugins/telemetry/server/index.ts | 3 +-
.../get_cluster_info.test.ts | 10 +-
.../get_cluster_stats.test.ts | 12 +-
.../get_data_telemetry.test.ts | 2 +-
.../server/telemetry_collection/get_kibana.ts | 6 +-
.../get_local_stats.test.ts | 19 +-
.../telemetry_collection/get_local_stats.ts | 4 +-
.../telemetry_collection/get_nodes_usage.ts | 12 +-
.../server/telemetry_collection/index.ts | 1 +
.../get_telemetry_saved_object.test.ts | 31 +-
.../server/encryption/encrypt.ts | 8 +-
.../server/plugin.ts | 10 +-
...telemetry_management_section.test.tsx.snap | 20 +-
.../components/opt_in_example_flyout.tsx | 4 +-
.../telemetry_management_section.test.tsx | 17 +-
.../telemetry_management_section.tsx | 41 +-
.../telemetry_management_section_wrapper.tsx | 12 +-
.../public/plugin.tsx | 15 +-
.../server/collector/collector.ts | 14 +-
.../server/collector/collector_set.test.ts | 20 +-
.../server/collector/collector_set.ts | 9 +-
.../server/collector/usage_collector.ts | 2 +
.../server/report/store_report.test.ts | 2 +-
.../server/routes/stats/stats.ts | 15 +-
.../telemetry_collection/get_license.test.ts | 6 +-
.../get_stats_with_xpack.test.ts | 31 +-
.../get_stats_with_xpack.ts | 4 +-
.../is_cluster_opted_in.test.ts | 6 +-
.../is_cluster_opted_in.ts | 4 +-
55 files changed, 1089 insertions(+), 348 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index 0f7af42fafbfd..8a6ea7957927a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1406,6 +1406,24 @@ module.exports = {
},
},
+ /**
+ * Do not allow `any`
+ */
+ {
+ files: [
+ 'packages/kbn-analytics/**',
+ // 'packages/kbn-telemetry-tools/**',
+ 'src/plugins/kibana_usage_collection/**',
+ 'src/plugins/usage_collection/**',
+ 'src/plugins/telemetry/**',
+ 'src/plugins/telemetry_collection_manager/**',
+ 'src/plugins/telemetry_management_section/**',
+ 'x-pack/plugins/telemetry_collection_xpack/**',
+ ],
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'error',
+ },
+ },
{
files: [
// core-team owned code
diff --git a/api_docs/telemetry.json b/api_docs/telemetry.json
index bff65ce9c68dd..bfb19a79bdb1e 100644
--- a/api_docs/telemetry.json
+++ b/api_docs/telemetry.json
@@ -1,9 +1,611 @@
{
"id": "telemetry",
"client": {
- "classes": [],
+ "classes": [
+ {
+ "id": "def-public.TelemetryNotifications",
+ "type": "Class",
+ "tags": [],
+ "label": "TelemetryNotifications",
+ "description": [],
+ "children": [
+ {
+ "id": "def-public.TelemetryNotifications.Unnamed",
+ "type": "Function",
+ "label": "Constructor",
+ "signature": [
+ "any"
+ ],
+ "description": [],
+ "children": [
+ {
+ "id": "def-public.TelemetryNotifications.Unnamed.$1",
+ "type": "Object",
+ "label": "{ http, overlays, telemetryService }",
+ "isRequired": true,
+ "signature": [
+ "TelemetryNotificationsConstructor"
+ ],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 27
+ }
+ }
+ ],
+ "tags": [],
+ "returnComment": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 27
+ }
+ },
+ {
+ "id": "def-public.TelemetryNotifications.shouldShowOptedInNoticeBanner",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => boolean"
+ ],
+ "description": [],
+ "label": "shouldShowOptedInNoticeBanner",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 33
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryNotifications.renderOptedInNoticeBanner",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => void"
+ ],
+ "description": [],
+ "label": "renderOptedInNoticeBanner",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 39
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryNotifications.shouldShowOptInBanner",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => boolean"
+ ],
+ "description": [],
+ "label": "shouldShowOptInBanner",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 49
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryNotifications.renderOptInBanner",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => void"
+ ],
+ "description": [],
+ "label": "renderOptInBanner",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 55
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryNotifications.setOptedInNoticeSeen",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => Promise"
+ ],
+ "description": [],
+ "label": "setOptedInNoticeSeen",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 73
+ },
+ "tags": [],
+ "returnComment": []
+ }
+ ],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts",
+ "lineNumber": 20
+ },
+ "initialIsOpen": false
+ },
+ {
+ "id": "def-public.TelemetryService",
+ "type": "Class",
+ "tags": [],
+ "label": "TelemetryService",
+ "description": [],
+ "children": [
+ {
+ "tags": [],
+ "id": "def-public.TelemetryService.currentKibanaVersion",
+ "type": "string",
+ "label": "currentKibanaVersion",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 28
+ }
+ },
+ {
+ "id": "def-public.TelemetryService.Unnamed",
+ "type": "Function",
+ "label": "Constructor",
+ "signature": [
+ "any"
+ ],
+ "description": [],
+ "children": [
+ {
+ "id": "def-public.TelemetryService.Unnamed.$1",
+ "type": "Object",
+ "label": "{\n config,\n http,\n notifications,\n currentKibanaVersion,\n reportOptInStatusChange = true,\n }",
+ "isRequired": true,
+ "signature": [
+ "TelemetryServiceConstructor"
+ ],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 30
+ }
+ }
+ ],
+ "tags": [],
+ "returnComment": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 30
+ }
+ },
+ {
+ "id": "def-public.TelemetryService.config",
+ "type": "Object",
+ "label": "config",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 44
+ },
+ "signature": [
+ {
+ "pluginId": "telemetry",
+ "scope": "public",
+ "docId": "kibTelemetryPluginApi",
+ "section": "def-public.TelemetryPluginConfig",
+ "text": "TelemetryPluginConfig"
+ }
+ ]
+ },
+ {
+ "id": "def-public.TelemetryService.config",
+ "type": "Object",
+ "label": "config",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 48
+ },
+ "signature": [
+ {
+ "pluginId": "telemetry",
+ "scope": "public",
+ "docId": "kibTelemetryPluginApi",
+ "section": "def-public.TelemetryPluginConfig",
+ "text": "TelemetryPluginConfig"
+ }
+ ]
+ },
+ {
+ "id": "def-public.TelemetryService.isOptedIn",
+ "type": "CompoundType",
+ "label": "isOptedIn",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 52
+ },
+ "signature": [
+ "boolean | null"
+ ]
+ },
+ {
+ "id": "def-public.TelemetryService.isOptedIn",
+ "type": "CompoundType",
+ "label": "isOptedIn",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 56
+ },
+ "signature": [
+ "boolean | null"
+ ]
+ },
+ {
+ "id": "def-public.TelemetryService.userHasSeenOptedInNotice",
+ "type": "CompoundType",
+ "label": "userHasSeenOptedInNotice",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 60
+ },
+ "signature": [
+ "boolean | undefined"
+ ]
+ },
+ {
+ "id": "def-public.TelemetryService.userHasSeenOptedInNotice",
+ "type": "CompoundType",
+ "label": "userHasSeenOptedInNotice",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 64
+ },
+ "signature": [
+ "boolean | undefined"
+ ]
+ },
+ {
+ "id": "def-public.TelemetryService.getCanChangeOptInStatus",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => boolean"
+ ],
+ "description": [],
+ "label": "getCanChangeOptInStatus",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 68
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.getOptInStatusUrl",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => string"
+ ],
+ "description": [],
+ "label": "getOptInStatusUrl",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 73
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.getTelemetryUrl",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => string"
+ ],
+ "description": [],
+ "label": "getTelemetryUrl",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 78
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.getUserShouldSeeOptInNotice",
+ "type": "Function",
+ "label": "getUserShouldSeeOptInNotice",
+ "signature": [
+ "() => boolean"
+ ],
+ "description": [
+ "\nReturns if an user should be shown the notice about Opt-In/Out telemetry.\nThe decision is made based on whether any user has already dismissed the message or\nthe user can't actually change the settings (in which case, there's no point on bothering them)"
+ ],
+ "children": [],
+ "tags": [],
+ "returnComment": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 88
+ }
+ },
+ {
+ "id": "def-public.TelemetryService.userCanChangeSettings",
+ "type": "boolean",
+ "label": "userCanChangeSettings",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 95
+ }
+ },
+ {
+ "id": "def-public.TelemetryService.userCanChangeSettings",
+ "type": "boolean",
+ "label": "userCanChangeSettings",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 99
+ }
+ },
+ {
+ "id": "def-public.TelemetryService.getIsOptedIn",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => boolean | null"
+ ],
+ "description": [],
+ "label": "getIsOptedIn",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 103
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.fetchExample",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => Promise"
+ ],
+ "description": [],
+ "label": "fetchExample",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 107
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.fetchTelemetry",
+ "type": "Function",
+ "children": [
+ {
+ "id": "def-public.TelemetryService.fetchTelemetry.$1",
+ "type": "Object",
+ "label": "{ unencrypted = false }",
+ "isRequired": true,
+ "signature": [
+ "{ unencrypted?: boolean | undefined; }"
+ ],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 111
+ }
+ }
+ ],
+ "signature": [
+ "({ unencrypted }?: { unencrypted?: boolean | undefined; }) => Promise"
+ ],
+ "description": [],
+ "label": "fetchTelemetry",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 111
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.setOptIn",
+ "type": "Function",
+ "children": [
+ {
+ "id": "def-public.TelemetryService.setOptIn.$1",
+ "type": "boolean",
+ "label": "optedIn",
+ "isRequired": true,
+ "signature": [
+ "boolean"
+ ],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 119
+ }
+ }
+ ],
+ "signature": [
+ "(optedIn: boolean) => Promise"
+ ],
+ "description": [],
+ "label": "setOptIn",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 119
+ },
+ "tags": [],
+ "returnComment": []
+ },
+ {
+ "id": "def-public.TelemetryService.setUserHasSeenNotice",
+ "type": "Function",
+ "children": [],
+ "signature": [
+ "() => Promise"
+ ],
+ "description": [],
+ "label": "setUserHasSeenNotice",
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 153
+ },
+ "tags": [],
+ "returnComment": []
+ }
+ ],
+ "source": {
+ "path": "src/plugins/telemetry/public/services/telemetry_service.ts",
+ "lineNumber": 21
+ },
+ "initialIsOpen": false
+ }
+ ],
"functions": [],
- "interfaces": [],
+ "interfaces": [
+ {
+ "id": "def-public.TelemetryPluginConfig",
+ "type": "Interface",
+ "label": "TelemetryPluginConfig",
+ "description": [],
+ "tags": [],
+ "children": [
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.enabled",
+ "type": "boolean",
+ "label": "enabled",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 46
+ }
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.url",
+ "type": "string",
+ "label": "url",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 47
+ }
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.banner",
+ "type": "boolean",
+ "label": "banner",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 48
+ }
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.allowChangingOptInStatus",
+ "type": "boolean",
+ "label": "allowChangingOptInStatus",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 49
+ }
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.optIn",
+ "type": "CompoundType",
+ "label": "optIn",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 50
+ },
+ "signature": [
+ "boolean | null"
+ ]
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.optInStatusUrl",
+ "type": "string",
+ "label": "optInStatusUrl",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 51
+ }
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.sendUsageFrom",
+ "type": "CompoundType",
+ "label": "sendUsageFrom",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 52
+ },
+ "signature": [
+ "\"browser\" | \"server\""
+ ]
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.telemetryNotifyUserAboutOptInDefault",
+ "type": "CompoundType",
+ "label": "telemetryNotifyUserAboutOptInDefault",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 53
+ },
+ "signature": [
+ "boolean | undefined"
+ ]
+ },
+ {
+ "tags": [],
+ "id": "def-public.TelemetryPluginConfig.userCanChangeSettings",
+ "type": "CompoundType",
+ "label": "userCanChangeSettings",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 54
+ },
+ "signature": [
+ "boolean | undefined"
+ ]
+ }
+ ],
+ "source": {
+ "path": "src/plugins/telemetry/public/plugin.ts",
+ "lineNumber": 45
+ },
+ "initialIsOpen": false
+ }
+ ],
"enums": [],
"misc": [],
"objects": [],
@@ -25,7 +627,13 @@
"lineNumber": 38
},
"signature": [
- "TelemetryService"
+ {
+ "pluginId": "telemetry",
+ "scope": "public",
+ "docId": "kibTelemetryPluginApi",
+ "section": "def-public.TelemetryService",
+ "text": "TelemetryService"
+ }
]
},
{
@@ -39,7 +647,13 @@
"lineNumber": 39
},
"signature": [
- "TelemetryNotifications"
+ {
+ "pluginId": "telemetry",
+ "scope": "public",
+ "docId": "kibTelemetryPluginApi",
+ "section": "def-public.TelemetryNotifications",
+ "text": "TelemetryNotifications"
+ }
]
},
{
@@ -82,7 +696,13 @@
"lineNumber": 34
},
"signature": [
- "TelemetryService"
+ {
+ "pluginId": "telemetry",
+ "scope": "public",
+ "docId": "kibTelemetryPluginApi",
+ "section": "def-public.TelemetryService",
+ "text": "TelemetryService"
+ }
]
}
],
@@ -95,137 +715,7 @@
}
},
"server": {
- "classes": [
- {
- "id": "def-server.FetcherTask",
- "type": "Class",
- "tags": [],
- "label": "FetcherTask",
- "description": [],
- "children": [
- {
- "id": "def-server.FetcherTask.Unnamed",
- "type": "Function",
- "label": "Constructor",
- "signature": [
- "any"
- ],
- "description": [],
- "children": [
- {
- "id": "def-server.FetcherTask.Unnamed.$1",
- "type": "Object",
- "label": "initializerContext",
- "isRequired": true,
- "signature": [
- {
- "pluginId": "core",
- "scope": "server",
- "docId": "kibCorePluginApi",
- "section": "def-server.PluginInitializerContext",
- "text": "PluginInitializerContext"
- },
- ">"
- ],
- "description": [],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 58
- }
- }
- ],
- "tags": [],
- "returnComment": [],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 58
- }
- },
- {
- "id": "def-server.FetcherTask.start",
- "type": "Function",
- "label": "start",
- "signature": [
- "({ savedObjects, elasticsearch }: ",
- {
- "pluginId": "core",
- "scope": "server",
- "docId": "kibCorePluginApi",
- "section": "def-server.CoreStart",
- "text": "CoreStart"
- },
- ", { telemetryCollectionManager }: ",
- "FetcherTaskDepsStart",
- ") => void"
- ],
- "description": [],
- "children": [
- {
- "id": "def-server.FetcherTask.start.$1",
- "type": "Object",
- "label": "{ savedObjects, elasticsearch }",
- "isRequired": true,
- "signature": [
- {
- "pluginId": "core",
- "scope": "server",
- "docId": "kibCorePluginApi",
- "section": "def-server.CoreStart",
- "text": "CoreStart"
- }
- ],
- "description": [],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 65
- }
- },
- {
- "id": "def-server.FetcherTask.start.$2",
- "type": "Object",
- "label": "{ telemetryCollectionManager }",
- "isRequired": true,
- "signature": [
- "FetcherTaskDepsStart"
- ],
- "description": [],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 66
- }
- }
- ],
- "tags": [],
- "returnComment": [],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 64
- }
- },
- {
- "id": "def-server.FetcherTask.stop",
- "type": "Function",
- "label": "stop",
- "signature": [
- "() => void"
- ],
- "description": [],
- "children": [],
- "tags": [],
- "returnComment": [],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 77
- }
- }
- ],
- "source": {
- "path": "src/plugins/telemetry/server/fetcher.ts",
- "lineNumber": 45
- },
- "initialIsOpen": false
- }
- ],
+ "classes": [],
"functions": [
{
"id": "def-server.buildDataTelemetryPayload",
@@ -420,15 +910,16 @@
"section": "def-server.StatsCollectionContext",
"text": "StatsCollectionContext"
},
- ") => Promise<{ timestamp: string; cluster_uuid: string; cluster_name: string; version: string; cluster_stats: any; collection: string; stack_stats: { data: ",
+ ") => Promise<{ timestamp: string; cluster_uuid: string; cluster_name: string; version: string; cluster_stats: Pick<{ nodes: { usage: { nodes: ",
{
"pluginId": "telemetry",
"scope": "server",
"docId": "kibTelemetryPluginApi",
- "section": "def-server.DataTelemetryPayload",
- "text": "DataTelemetryPayload"
+ "section": "def-server.NodeUsage",
+ "text": "NodeUsage"
},
- " | undefined; kibana: { count: number; indices: number; os: {}; versions: { version: string; count: number; }[]; plugins: { [plugin: string]: any; }; } | undefined; }; }[]>"
+ "[] | {}[]; }; count: ",
+ "ClusterNodeCount"
],
"description": [
"\nGet statistics for all products joined by Elasticsearch cluster."
@@ -656,6 +1147,123 @@
"lineNumber": 38
},
"initialIsOpen": false
+ },
+ {
+ "id": "def-server.NodeUsage",
+ "type": "Interface",
+ "label": "NodeUsage",
+ "description": [],
+ "tags": [],
+ "children": [
+ {
+ "tags": [],
+ "id": "def-server.NodeUsage.node_id",
+ "type": "string",
+ "label": "node_id",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 18
+ },
+ "signature": [
+ "string | undefined"
+ ]
+ },
+ {
+ "tags": [],
+ "id": "def-server.NodeUsage.timestamp",
+ "type": "CompoundType",
+ "label": "timestamp",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 19
+ },
+ "signature": [
+ "React.ReactText"
+ ]
+ },
+ {
+ "tags": [],
+ "id": "def-server.NodeUsage.since",
+ "type": "number",
+ "label": "since",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 20
+ }
+ },
+ {
+ "tags": [],
+ "id": "def-server.NodeUsage.rest_actions",
+ "type": "Object",
+ "label": "rest_actions",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 21
+ },
+ "signature": [
+ "{ [key: string]: number; }"
+ ]
+ },
+ {
+ "tags": [],
+ "id": "def-server.NodeUsage.aggregations",
+ "type": "Object",
+ "label": "aggregations",
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 24
+ },
+ "signature": [
+ "{ [key: string]: ",
+ {
+ "pluginId": "telemetry",
+ "scope": "server",
+ "docId": "kibTelemetryPluginApi",
+ "section": "def-server.NodeUsageAggregation",
+ "text": "NodeUsageAggregation"
+ },
+ "; } | undefined"
+ ]
+ }
+ ],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 17
+ },
+ "initialIsOpen": false
+ },
+ {
+ "id": "def-server.NodeUsageAggregation",
+ "type": "Interface",
+ "label": "NodeUsageAggregation",
+ "description": [],
+ "tags": [],
+ "children": [
+ {
+ "id": "def-server.NodeUsageAggregation.Unnamed",
+ "type": "Any",
+ "label": "Unnamed",
+ "tags": [],
+ "description": [],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 13
+ },
+ "signature": [
+ "any"
+ ]
+ }
+ ],
+ "source": {
+ "path": "src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts",
+ "lineNumber": 12
+ },
+ "initialIsOpen": false
}
],
"enums": [],
@@ -701,7 +1309,7 @@
"lineNumber": 51
},
"signature": [
- "{ timestamp: string; cluster_uuid: string; cluster_name: string; version: string; cluster_stats: any; collection: string; stack_stats: { data: DataTelemetryPayload | undefined; kibana: { count: number; indices: number; os: {}; versions: { version: string; count: number; }[]; plugins: { [plugin: string]: any; }; } | undefined; }; }"
+ "{ timestamp: string; cluster_uuid: string; cluster_name: string; version: string; cluster_stats: Pick; collection: string; stack_stats: { data: DataTelemetryPayload | undefined; kibana: { count: number; indices: number; os: {}; versions: { version: string; count: number; }[]; plugins: { [plugin: string]: Record; }; } | undefined; }; }"
],
"initialIsOpen": false
}
diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx
index bf91eb198f08e..f9a58d29ebd86 100644
--- a/api_docs/telemetry.mdx
+++ b/api_docs/telemetry.mdx
@@ -19,6 +19,12 @@ import telemetryObj from './telemetry.json';
### Start
+### Classes
+
+
+### Interfaces
+
+
## Server
### Setup
@@ -30,9 +36,6 @@ import telemetryObj from './telemetry.json';
### Functions
-### Classes
-
-
### Interfaces
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md
index 193a2e5a24f3f..5fffc5436e9c6 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md
@@ -7,7 +7,7 @@
Signature:
```typescript
-SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
- WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>;
+SearchBar: React.ComponentClass, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "isClearable" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
+ WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>;
}
```
diff --git a/packages/kbn-analytics/src/reporter.ts b/packages/kbn-analytics/src/reporter.ts
index 44e6758eb4643..c7c9cf1541d21 100644
--- a/packages/kbn-analytics/src/reporter.ts
+++ b/packages/kbn-analytics/src/reporter.ts
@@ -68,7 +68,7 @@ export class Reporter {
}
};
- private log(message: any) {
+ private log(message: unknown) {
if (this.debug) {
// eslint-disable-next-line
console.debug(message);
diff --git a/packages/kbn-analytics/src/storage.ts b/packages/kbn-analytics/src/storage.ts
index b080a53029724..ac1084e807fc7 100644
--- a/packages/kbn-analytics/src/storage.ts
+++ b/packages/kbn-analytics/src/storage.ts
@@ -8,10 +8,10 @@
import { Report } from './report';
-export interface Storage {
- get: (key: string) => T | null;
+export interface Storage {
+ get: (key: string) => T | undefined;
set: (key: string, value: T) => S;
- remove: (key: string) => T | null;
+ remove: (key: string) => T | undefined;
clear: () => void;
}
diff --git a/packages/kbn-analytics/src/util.ts b/packages/kbn-analytics/src/util.ts
index 96e18c43e104f..b3768b4df94b8 100644
--- a/packages/kbn-analytics/src/util.ts
+++ b/packages/kbn-analytics/src/util.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export function wrapArray(subj: T | T[]): T[] {
+export function wrapArray(subj: T | T[]): T[] {
return Array.isArray(subj) ? subj : [subj];
}
diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts
index 50b39114d2143..854a70ae48a97 100644
--- a/src/plugins/advanced_settings/public/management_app/types.ts
+++ b/src/plugins/advanced_settings/public/management_app/types.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import type { ReactElement } from 'react';
import { UiCounterMetricType } from '@kbn/analytics';
import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public';
@@ -13,7 +14,7 @@ export interface FieldSetting {
displayName: string;
name: string;
value: unknown;
- description?: string;
+ description?: string | ReactElement;
options?: string[];
optionLabels?: Record;
requiresPageReload: boolean;
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 15d3f5c403b1f..a69df282cd568 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -32,7 +32,6 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { ExpressionValueBoxed } from 'src/plugins/expressions/common';
import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils';
import { IAggConfigs as IAggConfigs_2 } from 'src/plugins/data/public';
-import { ISavedObjectsRepository } from 'src/core/server';
import { IScopedClusterClient } from 'src/core/server';
import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public';
import { ISearchSource } from 'src/plugins/data/public';
diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts
index da4e1b101914f..38864945a17d0 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts
@@ -116,10 +116,15 @@ describe('telemetry_application_usage', () => {
minutesOnScreen: 10,
numberOfClicks: 10,
},
+ type: opts.type,
+ references: [],
+ score: 0,
},
],
total: 1,
- } as any;
+ per_page: 10,
+ page: 1,
+ };
case SAVED_OBJECTS_DAILY_TYPE:
return {
saved_objects: [
@@ -131,9 +136,21 @@ describe('telemetry_application_usage', () => {
minutesOnScreen: 0.5,
numberOfClicks: 1,
},
+ type: opts.type,
+ references: [],
+ score: 0,
},
],
total: 1,
+ per_page: 10,
+ page: 1,
+ };
+ default:
+ return {
+ saved_objects: [],
+ total: 0,
+ per_page: 10,
+ page: 1,
};
}
});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/config_usage/register_config_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/config_usage/register_config_usage_collector.test.ts
index 7d4f03fd30edf..bc6f8c956b669 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/config_usage/register_config_usage_collector.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/config_usage/register_config_usage_collector.test.ts
@@ -28,7 +28,7 @@ describe('kibana_config_usage', () => {
const collectorFetchContext = createCollectorFetchContextMock();
const coreUsageDataStart = coreUsageDataServiceMock.createStartContract();
- const mockConfigUsage = (Symbol('config usage telemetry') as any) as ConfigUsageData;
+ const mockConfigUsage = (Symbol('config usage telemetry') as unknown) as ConfigUsageData;
coreUsageDataStart.getConfigsUsageData.mockResolvedValue(mockConfigUsage);
beforeAll(() => registerConfigUsageCollector(usageCollectionMock, () => coreUsageDataStart));
diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.test.ts
index b671a9f93d369..5410e491a85fd 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.test.ts
@@ -28,7 +28,7 @@ describe('telemetry_core', () => {
const collectorFetchContext = createCollectorFetchContextMock();
const coreUsageDataStart = coreUsageDataServiceMock.createStartContract();
- const getCoreUsageDataReturnValue = (Symbol('core telemetry') as any) as CoreUsageData;
+ const getCoreUsageDataReturnValue = (Symbol('core telemetry') as unknown) as CoreUsageData;
coreUsageDataStart.getCoreUsageData.mockResolvedValue(getCoreUsageDataReturnValue);
beforeAll(() => registerCoreUsageCollector(usageCollectionMock, () => coreUsageDataStart));
diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts
index 15cbecde386f7..3d5d7854d6f9e 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.test.ts
@@ -9,13 +9,11 @@
import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks';
import { getSavedObjectsCounts } from './get_saved_object_counts';
-export function mockGetSavedObjectsCounts(params: any) {
+export function mockGetSavedObjectsCounts(params: TBody) {
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
esClient.search.mockResolvedValue(
// @ts-expect-error we only care about the response body
- {
- body: { ...params },
- }
+ { body: { ...params } }
);
return esClient;
}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts
index e1afbfbcecc4e..2c75d3edc3a84 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts
@@ -34,7 +34,8 @@ describe('telemetry_kibana', () => {
const getMockFetchClients = (hits?: unknown[]) => {
const fetchParamsMock = createCollectorFetchContextMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- esClient.search.mockResolvedValue({ body: { hits: { hits } } } as any);
+ // @ts-expect-error for the sake of the tests, we only require `hits`
+ esClient.search.mockResolvedValue({ body: { hits: { hits } } });
fetchParamsMock.esClient = esClient;
return fetchParamsMock;
};
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts
index cb0b1c045397d..8295342c527ab 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.test.ts
@@ -17,6 +17,7 @@ import {
registerManagementUsageCollector,
createCollectorFetch,
} from './telemetry_management_collector';
+import { IUiSettingsClient } from 'kibana/server';
const logger = loggingSystemMock.createLogger();
@@ -30,7 +31,7 @@ describe('telemetry_application_usage_collector', () => {
});
const uiSettingsClient = uiSettingsServiceMock.createClient();
- const getUiSettingsClient = jest.fn(() => uiSettingsClient);
+ const getUiSettingsClient = jest.fn((): IUiSettingsClient | undefined => uiSettingsClient);
const mockedFetchContext = createCollectorFetchContextMock();
beforeAll(() => {
@@ -42,7 +43,7 @@ describe('telemetry_application_usage_collector', () => {
});
test('isReady() => false if no client', () => {
- getUiSettingsClient.mockImplementationOnce(() => undefined as any);
+ getUiSettingsClient.mockImplementationOnce(() => undefined);
expect(collector.isReady()).toBe(false);
});
@@ -60,7 +61,7 @@ describe('telemetry_application_usage_collector', () => {
});
test('fetch() should not fail if invoked when not ready', async () => {
- getUiSettingsClient.mockImplementationOnce(() => undefined as any);
+ getUiSettingsClient.mockImplementationOnce(() => undefined);
await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined);
});
});
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts
index cba5140997f3f..7dd1a4dc4410e 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts
@@ -22,12 +22,13 @@ export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClien
const userProvided = await uiSettingsClient.getUserProvided();
const modifiedEntries = Object.entries(userProvided)
.filter(([key]) => key !== 'buildNum')
- .reduce((obj: any, [key, { userValue }]) => {
+ .reduce((obj: Record, [key, { userValue }]) => {
const sensitive = uiSettingsClient.isSensitive(key);
obj[key] = sensitive ? REDACTED_KEYWORD : userValue;
return obj;
}, {});
- return modifiedEntries;
+ // TODO: It would be Partial, but the telemetry-tools for the schema extraction still does not support it. We need to fix it before setting the right Partial type
+ return (modifiedEntries as unknown) as UsageStats;
};
}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts
index 51ecbf736bfc1..31cb869d14e57 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts
@@ -30,6 +30,8 @@ describe('telemetry_ui_metric', () => {
const registerType = jest.fn();
const mockedFetchContext = createCollectorFetchContextMock();
+ const commonSavedObjectsAttributes = { score: 0, references: [], type: 'ui-metric' };
+
beforeAll(() =>
registerUiMetricUsageCollector(usageCollectionMock, registerType, getUsageCollector)
);
@@ -44,13 +46,12 @@ describe('telemetry_ui_metric', () => {
test('when savedObjectClient is initialised, return something', async () => {
const savedObjectClient = savedObjectsRepositoryMock.create();
- savedObjectClient.find.mockImplementation(
- async () =>
- ({
- saved_objects: [],
- total: 0,
- } as any)
- );
+ savedObjectClient.find.mockImplementation(async () => ({
+ saved_objects: [],
+ total: 0,
+ per_page: 10,
+ page: 1,
+ }));
getUsageCollector.mockImplementation(() => savedObjectClient);
expect(await collector.fetch(mockedFetchContext)).toStrictEqual({});
@@ -59,20 +60,33 @@ describe('telemetry_ui_metric', () => {
test('results grouped by appName', async () => {
const savedObjectClient = savedObjectsRepositoryMock.create();
- savedObjectClient.find.mockImplementation(async () => {
- return {
- saved_objects: [
- { id: 'testAppName:testKeyName1', attributes: { count: 3 } },
- { id: 'testAppName:testKeyName2', attributes: { count: 5 } },
- { id: 'testAppName2:testKeyName3', attributes: { count: 1 } },
- {
- id:
- 'kibana-user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
- attributes: { count: 1 },
- },
- ],
- total: 3,
- } as any;
+ savedObjectClient.find.mockResolvedValue({
+ saved_objects: [
+ {
+ ...commonSavedObjectsAttributes,
+ id: 'testAppName:testKeyName1',
+ attributes: { count: 3 },
+ },
+ {
+ ...commonSavedObjectsAttributes,
+ id: 'testAppName:testKeyName2',
+ attributes: { count: 5 },
+ },
+ {
+ ...commonSavedObjectsAttributes,
+ id: 'testAppName2:testKeyName3',
+ attributes: { count: 1 },
+ },
+ {
+ ...commonSavedObjectsAttributes,
+ id:
+ 'kibana-user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
+ attributes: { count: 1 },
+ },
+ ],
+ total: 3,
+ per_page: 3,
+ page: 1,
});
getUsageCollector.mockImplementation(() => savedObjectClient);
diff --git a/src/plugins/telemetry/common/telemetry_config/get_telemetry_failure_details.test.ts b/src/plugins/telemetry/common/telemetry_config/get_telemetry_failure_details.test.ts
index 617b5189de4a8..c93ba53230954 100644
--- a/src/plugins/telemetry/common/telemetry_config/get_telemetry_failure_details.test.ts
+++ b/src/plugins/telemetry/common/telemetry_config/get_telemetry_failure_details.test.ts
@@ -35,9 +35,10 @@ describe('getTelemetryFailureDetails: get details about server usage fetcher fai
expect(
getTelemetryFailureDetails({
telemetrySavedObject: {
+ // @ts-expect-error the test is intentionally testing malformed SOs
reportFailureCount: null,
reportFailureVersion: failureVersion,
- } as any,
+ },
})
).toStrictEqual({ failureVersion, failureCount: 0 });
expect(
@@ -51,9 +52,10 @@ describe('getTelemetryFailureDetails: get details about server usage fetcher fai
expect(
getTelemetryFailureDetails({
telemetrySavedObject: {
+ // @ts-expect-error the test is intentionally testing malformed SOs
reportFailureCount: 'not_a_number',
reportFailureVersion: failureVersion,
- } as any,
+ },
})
).toStrictEqual({ failureVersion, failureCount: 0 });
});
@@ -63,9 +65,10 @@ describe('getTelemetryFailureDetails: get details about server usage fetcher fai
expect(
getTelemetryFailureDetails({
telemetrySavedObject: {
+ // @ts-expect-error the test is intentionally testing malformed SOs
reportFailureVersion: null,
reportFailureCount: failureCount,
- } as any,
+ },
})
).toStrictEqual({ failureCount, failureVersion: undefined });
expect(
@@ -76,9 +79,10 @@ describe('getTelemetryFailureDetails: get details about server usage fetcher fai
expect(
getTelemetryFailureDetails({
telemetrySavedObject: {
+ // @ts-expect-error the test is intentionally testing malformed SOs
reportFailureVersion: 123,
reportFailureCount: failureCount,
- } as any,
+ },
})
).toStrictEqual({ failureCount, failureVersion: undefined });
});
diff --git a/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.test.ts b/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.test.ts
index 65e4a2d43eef7..ede56688e0449 100644
--- a/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.test.ts
+++ b/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.test.ts
@@ -55,6 +55,7 @@ describe('getTelemetryOptIn', () => {
// build a table of tests with version checks, with results for enabled false
type VersionCheckTable = Array>;
+ // @ts-expect-error the test is intentionally testing malformed objects
const EnabledFalseVersionChecks: VersionCheckTable = [
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '8.0.0', result: false },
{ lastVersionChecked: '8.0.0', currentKibanaVersion: '8.0.1', result: false },
@@ -112,7 +113,7 @@ describe('getTelemetryOptIn', () => {
interface CallGetTelemetryOptInParams {
savedObjectNotFound: boolean;
savedObjectForbidden: boolean;
- lastVersionChecked?: any; // should be a string, but test with non-strings
+ lastVersionChecked?: string; // should be a string, but test with non-strings
currentKibanaVersion: string;
result?: boolean | null;
enabled: boolean | null | undefined;
diff --git a/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx
index b91f6ee9e4b51..a10c26c22e3fa 100644
--- a/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx
+++ b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx
@@ -17,7 +17,7 @@ import { HttpSetup } from '../../../../core/public';
interface Props {
http: HttpSetup;
- onSeenBanner: () => any;
+ onSeenBanner: () => unknown;
}
export class OptedInNoticeBanner extends React.PureComponent {
diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts
index 47ba7828eaec2..aef955e228dd3 100644
--- a/src/plugins/telemetry/public/index.ts
+++ b/src/plugins/telemetry/public/index.ts
@@ -8,7 +8,8 @@
import { PluginInitializerContext } from 'kibana/public';
import { TelemetryPlugin, TelemetryPluginConfig } from './plugin';
-export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
+export type { TelemetryPluginStart, TelemetryPluginSetup, TelemetryPluginConfig } from './plugin';
+export type { TelemetryNotifications, TelemetryService } from './services';
export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryPlugin(initializerContext);
diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx
index 4180f577e3037..f880aef3e3235 100644
--- a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx
+++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx
@@ -13,7 +13,7 @@ import { toMountPoint } from '../../../../kibana_react/public';
interface RenderBannerConfig {
overlays: CoreStart['overlays'];
- setOptIn: (isOptIn: boolean) => Promise;
+ setOptIn: (isOptIn: boolean) => Promise;
}
export function renderOptInBanner({ setOptIn, overlays }: RenderBannerConfig) {
diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts
index 05588f4c9e704..937416d283872 100644
--- a/src/plugins/telemetry/public/services/telemetry_sender.ts
+++ b/src/plugins/telemetry/public/services/telemetry_sender.ts
@@ -58,8 +58,8 @@ export class TelemetrySender {
this.isSending = true;
try {
const telemetryUrl = this.telemetryService.getTelemetryUrl();
- const telemetryData: any | any[] = await this.telemetryService.fetchTelemetry();
- const clusters: string[] = [].concat(telemetryData);
+ const telemetryData: string | string[] = await this.telemetryService.fetchTelemetry();
+ const clusters: string[] = ([] as string[]).concat(telemetryData);
await Promise.all(
clusters.map(
async (cluster) =>
diff --git a/src/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts b/src/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts
index 6b79cc6c7410a..c5624d1f62bf7 100644
--- a/src/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts
+++ b/src/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts
@@ -8,12 +8,15 @@
//
// THIS IS A DIRECT COPY OF
-// '../../../../../../../../src/core/server/config/ensure_deep_object'
+// 'packages/kbn-config/src/raw/ensure_deep_object.ts'
// BECAUSE THAT IS BLOCKED FOR IMPORTING BY OUR LINTER.
//
// IF THAT IS EXPOSED, WE SHOULD USE IT RATHER THAN CLONE IT.
//
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// ^ Disabling the rule for the entire file because of the complexity to type this
+
const separator = '.';
/**
diff --git a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts
index ac439f0753a2b..2acc6676d13db 100644
--- a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts
+++ b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts
@@ -15,10 +15,11 @@ import {
readTelemetryFile,
MAX_FILE_SIZE,
} from './telemetry_usage_collector';
+import { usageCollectionPluginMock } from '../../../../usage_collection/server/mocks';
-const mockUsageCollector = () => ({
- makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg),
-});
+const mockUsageCollector = () => {
+ return usageCollectionPluginMock.createSetupContract();
+};
describe('telemetry_usage_collector', () => {
const tempDir = tmpdir();
@@ -105,14 +106,15 @@ describe('telemetry_usage_collector', () => {
// dir
// the `makeUsageCollector` is mocked above to return the argument passed to it
- const usageCollector = mockUsageCollector() as any;
+ const usageCollector = mockUsageCollector();
const collectorOptions = createTelemetryUsageCollector(
usageCollector,
async () => tempFiles.unreadable
);
expect(collectorOptions.type).toBe('static_telemetry');
- expect(await collectorOptions.fetch({} as any)).toEqual(expectedObject); // Sending any as the callCluster client because it's not needed in this collector but TS requires it when calling it.
+ // @ts-expect-error this collector does not require any arguments in the fetch method, but TS complains
+ expect(await collectorOptions.fetch()).toEqual(expectedObject);
});
});
});
diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts
index 5db1b62cb3e26..fb188a2414b98 100644
--- a/src/plugins/telemetry/server/fetcher.ts
+++ b/src/plugins/telemetry/server/fetcher.ts
@@ -9,10 +9,7 @@
import { Observable, Subscription, timer } from 'rxjs';
import { take } from 'rxjs/operators';
import fetch from 'node-fetch';
-import {
- TelemetryCollectionManagerPluginStart,
- UsageStatsPayload,
-} from 'src/plugins/telemetry_collection_manager/server';
+import type { TelemetryCollectionManagerPluginStart } from 'src/plugins/telemetry_collection_manager/server';
import {
PluginInitializerContext,
Logger,
@@ -40,6 +37,7 @@ interface TelemetryConfig {
telemetryUrl: string;
failureCount: number;
failureVersion: string | undefined;
+ currentVersion: string;
}
export class FetcherTask {
@@ -104,7 +102,7 @@ export class FetcherTask {
return;
}
- let clusters: Array = [];
+ let clusters: string[] = [];
this.isSending = true;
try {
@@ -160,6 +158,7 @@ export class FetcherTask {
telemetryUrl,
failureCount,
failureVersion,
+ currentVersion: currentKibanaVersion,
};
}
@@ -187,11 +186,11 @@ export class FetcherTask {
private shouldSendReport({
telemetryOptIn,
telemetrySendUsageFrom,
- reportFailureCount,
+ failureCount,
+ failureVersion,
currentVersion,
- reportFailureVersion,
- }: any) {
- if (reportFailureCount > 2 && reportFailureVersion === currentVersion) {
+ }: TelemetryConfig) {
+ if (failureCount > 2 && failureVersion === currentVersion) {
return false;
}
@@ -209,7 +208,7 @@ export class FetcherTask {
});
}
- private async sendTelemetry(url: string, cluster: any): Promise {
+ private async sendTelemetry(url: string, cluster: string): Promise {
this.logger.debug(`Sending usage stats.`);
/**
* send OPTIONS before sending usage data.
diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts
index 1c335426ffd03..005f50721e778 100644
--- a/src/plugins/telemetry/server/index.ts
+++ b/src/plugins/telemetry/server/index.ts
@@ -11,7 +11,6 @@ import { TelemetryPlugin } from './plugin';
import * as constants from '../common/constants';
import { configSchema, TelemetryConfigType } from './config';
-export { FetcherTask } from './fetcher';
export { handleOldSettings } from './handle_old_settings';
export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
@@ -42,4 +41,6 @@ export type {
TelemetryLocalStats,
DataTelemetryIndex,
DataTelemetryPayload,
+ NodeUsage,
+ NodeUsageAggregation,
} from './telemetry_collection';
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts
index 9e70e31925226..cd414beb42182 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts
@@ -9,14 +9,10 @@
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { getClusterInfo } from './get_cluster_info';
-export function mockGetClusterInfo(clusterInfo: any) {
+export function mockGetClusterInfo(clusterInfo: ClusterInfo) {
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- esClient.info.mockResolvedValue(
- // @ts-expect-error we only care about the response body
- {
- body: { ...clusterInfo },
- }
- );
+ // @ts-expect-error we only care about the response body
+ esClient.info.mockResolvedValue({ body: { ...clusterInfo } });
return esClient;
}
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts
index a2c22fbbb0a78..06d3ebeb7ea0e 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts
@@ -10,15 +10,9 @@ import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { getClusterStats } from './get_cluster_stats';
import { TIMEOUT } from './constants';
-export function mockGetClusterStats(clusterStats: any) {
- const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- esClient.cluster.stats.mockResolvedValue(clusterStats);
- return esClient;
-}
-
describe('get_cluster_stats', () => {
it('uses the esClient to get the response from the `cluster.stats` API', async () => {
- const response = Promise.resolve({ body: { cluster_uuid: '1234' } });
+ const response = { body: { cluster_uuid: '1234' } };
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
esClient.cluster.stats.mockImplementationOnce(
// @ts-expect-error the method only cares about the response body
@@ -26,8 +20,8 @@ describe('get_cluster_stats', () => {
return response;
}
);
- const result = getClusterStats(esClient);
+ const result = await getClusterStats(esClient);
expect(esClient.cluster.stats).toHaveBeenCalledWith({ timeout: TIMEOUT });
- expect(result).toStrictEqual(response);
+ expect(result).toStrictEqual(response.body);
});
});
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts
index d2113dce9548f..dab1eaeed27ce 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts
@@ -271,7 +271,7 @@ describe('get_data_telemetry', () => {
function mockEsClient(
indicesMappings: string[] = [], // an array of `indices` to get mappings from.
{ isECS = false, dataStreamDataset = '', dataStreamType = '', shipper = '' } = {},
- indexStats: any = {}
+ indexStats = {}
) {
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
// @ts-expect-error
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts
index 566c942890150..3f1966901544a 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts
@@ -8,7 +8,7 @@
import { omit } from 'lodash';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { ISavedObjectsRepository, KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
+import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
import { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server';
import { ElasticsearchClient } from 'src/core/server';
@@ -27,7 +27,7 @@ export interface KibanaUsageStats {
};
};
- [plugin: string]: any;
+ [plugin: string]: Record;
}
export function handleKibanaStats(
@@ -73,7 +73,7 @@ export function handleKibanaStats(
export async function getKibana(
usageCollection: UsageCollectionSetup,
asInternalUser: ElasticsearchClient,
- soClient: SavedObjectsClientContract | ISavedObjectsRepository,
+ soClient: SavedObjectsClientContract,
kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter
): Promise {
const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest);
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts
index edf8dbb30809b..7fd6ca4080d6a 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts
@@ -20,17 +20,18 @@ import { StatsCollectionConfig } from '../../../telemetry_collection_manager/ser
function mockUsageCollection(kibanaUsage = {}) {
const usageCollection = usageCollectionPluginMock.createSetupContract();
usageCollection.bulkFetch = jest.fn().mockResolvedValue(kibanaUsage);
- usageCollection.toObject = jest.fn().mockImplementation((data: any) => data);
+ usageCollection.toObject = jest.fn().mockImplementation((data) => data);
return usageCollection;
}
// set up successful call mocks for info, cluster stats, nodes usage and data telemetry
-function mockGetLocalStats(clusterInfo: any, clusterStats: any) {
+function mockGetLocalStats(
+ clusterInfo: ClusterInfo,
+ clusterStats: ClusterStats
+) {
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
esClient.info.mockResolvedValue(
// @ts-expect-error we only care about the response body
- {
- body: { ...clusterInfo },
- }
+ { body: { ...clusterInfo } }
);
esClient.cluster.stats
// @ts-expect-error we only care about the response body
@@ -70,8 +71,8 @@ function mockGetLocalStats(clusterInfo: any, clusterStats: any) {
}
function mockStatsCollectionConfig(
- clusterInfo: any,
- clusterStats: any,
+ clusterInfo: unknown,
+ clusterStats: unknown,
kibana: {}
): StatsCollectionConfig {
return {
@@ -113,13 +114,13 @@ describe('get_local_stats', () => {
},
},
];
- const clusterStats = {
+ const clusterStats = ({
_nodes: { failed: 123 },
cluster_name: 'real-cool',
indices: { totally: 456 },
nodes: { yup: 'abc' },
random: 123,
- };
+ } as unknown) as estypes.ClusterStatsResponse;
const kibana = {
kibana: {
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts
index 67f9ebb8ff3e4..72f6ba855096c 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts
@@ -26,10 +26,10 @@ import { getDataTelemetry, DATA_TELEMETRY_ID, DataTelemetryPayload } from './get
* @param {Object} clusterStats Cluster stats (GET /_cluster/stats)
* @param {Object} kibana The Kibana Usage stats
*/
-export function handleLocalStats(
+export function handleLocalStats(
// eslint-disable-next-line @typescript-eslint/naming-convention
{ cluster_name, cluster_uuid, version }: estypes.RootNodeInfoResponse,
- { _nodes, cluster_name: clusterName, ...clusterStats }: any,
+ { _nodes, cluster_name: clusterName, ...clusterStats }: ClusterStats,
kibana: KibanaUsageStats | undefined,
dataTelemetry: DataTelemetryPayload | undefined,
context: StatsCollectionContext
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts
index e46d4be540734..544142c8d742f 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts
@@ -9,12 +9,12 @@
import { ElasticsearchClient } from 'src/core/server';
import { TIMEOUT } from './constants';
-export interface NodeAggregation {
+export interface NodeUsageAggregation {
[key: string]: number;
}
// we set aggregations as an optional type because it was only added in v7.8.0
-export interface NodeObj {
+export interface NodeUsage {
node_id?: string;
timestamp: number | string;
since: number;
@@ -22,20 +22,20 @@ export interface NodeObj {
[key: string]: number;
};
aggregations?: {
- [key: string]: NodeAggregation;
+ [key: string]: NodeUsageAggregation;
};
}
export interface NodesFeatureUsageResponse {
cluster_name: string;
nodes: {
- [key: string]: NodeObj;
+ [key: string]: NodeUsage;
};
}
export type NodesUsageGetter = (
esClient: ElasticsearchClient
-) => Promise<{ nodes: NodeObj[] | Array<{}> }>;
+) => Promise<{ nodes: NodeUsage[] | Array<{}> }>;
/**
* Get the nodes usage data from the connected cluster.
*
@@ -61,7 +61,7 @@ export async function fetchNodesUsage(
export const getNodesUsage: NodesUsageGetter = async (esClient) => {
const result = await fetchNodesUsage(esClient);
const transformedNodes = Object.entries(result?.nodes || {}).map(([key, value]) => ({
- ...(value as NodeObj),
+ ...(value as NodeUsage),
node_id: key,
}));
return { nodes: transformedNodes };
diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts
index 151e89a11a192..f55147a0a083f 100644
--- a/src/plugins/telemetry/server/telemetry_collection/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/index.ts
@@ -10,5 +10,6 @@ export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemet
export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry';
export { getLocalStats } from './get_local_stats';
export type { TelemetryLocalStats } from './get_local_stats';
+export type { NodeUsage, NodeUsageAggregation } from './get_nodes_usage';
export { getClusterUuids } from './get_cluster_stats';
export { registerCollection } from './register_collection';
diff --git a/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts b/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts
index fbead0125fe09..5ea8211739a13 100644
--- a/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts
+++ b/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts
@@ -8,6 +8,7 @@
import { getTelemetrySavedObject } from './get_telemetry_saved_object';
import { SavedObjectsErrorHelpers } from '../../../../core/server';
+import { savedObjectsClientMock } from '../../../../core/server/mocks';
describe('getTelemetrySavedObject', () => {
it('returns null when saved object not found', async () => {
@@ -51,7 +52,7 @@ interface CallGetTelemetrySavedObjectParams {
savedObjectNotFound: boolean;
savedObjectForbidden: boolean;
savedObjectOtherError: boolean;
- result?: any;
+ result?: unknown;
}
const DefaultParams = {
@@ -68,26 +69,22 @@ function getCallGetTelemetrySavedObjectParams(
async function callGetTelemetrySavedObject(params: CallGetTelemetrySavedObjectParams) {
const savedObjectsClient = getMockSavedObjectsClient(params);
- return await getTelemetrySavedObject(savedObjectsClient as any);
+ return await getTelemetrySavedObject(savedObjectsClient);
}
const SavedObjectForbiddenMessage = 'savedObjectForbidden';
const SavedObjectOtherErrorMessage = 'savedObjectOtherError';
function getMockSavedObjectsClient(params: CallGetTelemetrySavedObjectParams) {
- return {
- async get(type: string, id: string) {
- if (params.savedObjectNotFound) throw SavedObjectsErrorHelpers.createGenericNotFoundError();
- if (params.savedObjectForbidden)
- throw SavedObjectsErrorHelpers.decorateForbiddenError(
- new Error(SavedObjectForbiddenMessage)
- );
- if (params.savedObjectOtherError)
- throw SavedObjectsErrorHelpers.decorateGeneralError(
- new Error(SavedObjectOtherErrorMessage)
- );
-
- return { attributes: { enabled: null } };
- },
- };
+ const savedObjectsClient = savedObjectsClientMock.create();
+ savedObjectsClient.get.mockImplementation(async (type, id) => {
+ if (params.savedObjectNotFound) throw SavedObjectsErrorHelpers.createGenericNotFoundError();
+ if (params.savedObjectForbidden)
+ throw SavedObjectsErrorHelpers.decorateForbiddenError(new Error(SavedObjectForbiddenMessage));
+ if (params.savedObjectOtherError)
+ throw SavedObjectsErrorHelpers.decorateGeneralError(new Error(SavedObjectOtherErrorMessage));
+
+ return { id, type, attributes: { enabled: null }, references: [] };
+ });
+ return savedObjectsClient;
}
diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts
index a2c24627f6fd7..1b80a2c29b362 100644
--- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts
+++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts
@@ -13,12 +13,12 @@ export function getKID(useProdKey = false): string {
return useProdKey ? 'kibana1' : 'kibana_dev1';
}
-export async function encryptTelemetry(
- payload: any,
+export async function encryptTelemetry(
+ payload: Payload | Payload[],
{ useProdKey = false } = {}
): Promise {
const kid = getKID(useProdKey);
const encryptor = await createRequestEncryptor(telemetryJWKS);
- const clusters = [].concat(payload);
- return Promise.all(clusters.map((cluster: any) => encryptor.encrypt(kid, cluster)));
+ const clusters = ([] as Payload[]).concat(payload);
+ return Promise.all(clusters.map((cluster) => encryptor.encrypt(kid, cluster)));
}
diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts
index 692d91b963d9d..0efdde5eeafd6 100644
--- a/src/plugins/telemetry_collection_manager/server/plugin.ts
+++ b/src/plugins/telemetry_collection_manager/server/plugin.ts
@@ -7,7 +7,7 @@
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import {
+import type {
PluginInitializerContext,
CoreSetup,
CoreStart,
@@ -19,7 +19,7 @@ import {
SavedObjectsClientContract,
} from 'src/core/server';
-import {
+import type {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
BasicStatsPayload,
@@ -29,6 +29,8 @@ import {
StatsCollectionConfig,
UsageStatsPayload,
StatsCollectionContext,
+ UnencryptedStatsGetterConfig,
+ EncryptedStatsGetterConfig,
} from './types';
import { encryptTelemetry } from './encryption';
import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client';
@@ -40,7 +42,7 @@ interface TelemetryCollectionPluginsDepsSetup {
export class TelemetryCollectionManagerPlugin
implements Plugin {
private readonly logger: Logger;
- private collectionStrategy: CollectionStrategy | undefined;
+ private collectionStrategy: CollectionStrategy | undefined;
private usageGetterMethodPriority = -1;
private usageCollection?: UsageCollectionSetup;
private elasticsearchClient?: IClusterClient;
@@ -215,6 +217,8 @@ export class TelemetryCollectionManagerPlugin
}));
};
+ private async getStats(config: UnencryptedStatsGetterConfig): Promise;
+ private async getStats(config: EncryptedStatsGetterConfig): Promise;
private async getStats(config: StatsGetterConfig) {
if (!this.usageCollection) {
return [];
diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap
index 896b1671328a9..e0ba7e7527af7 100644
--- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap
+++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap
@@ -95,13 +95,14 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
size="s"
/>
@@ -156,12 +157,26 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
,
"displayName": "Provide usage statistics",
+ "isCustom": true,
+ "isOverridden": false,
"name": "telemetry:enabled",
+ "requiresPageReload": false,
"type": "boolean",
"value": true,
}
}
- toasts={null}
+ toasts={
+ Object {
+ "add": [MockFunction],
+ "addDanger": [MockFunction],
+ "addError": [MockFunction],
+ "addInfo": [MockFunction],
+ "addSuccess": [MockFunction],
+ "addWarning": [MockFunction],
+ "get$": [MockFunction],
+ "remove": [MockFunction],
+ }
+ }
/>
@@ -170,6 +185,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `
exports[`TelemetryManagementSectionComponent renders null because allowChangingOptInStatus is false 1`] = `
Promise;
+ fetchExample: () => Promise;
onClose: () => void;
}
interface State {
isLoading: boolean;
hasPrivilegeToRead: boolean;
- data: any[] | null;
+ data: unknown[] | null;
}
/**
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx
index 7e7e255edea8c..019dedd793fa2 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx
@@ -12,9 +12,11 @@ import TelemetryManagementSection from './telemetry_management_section';
import { TelemetryService } from '../../../telemetry/public/services';
import { coreMock } from '../../../../core/public/mocks';
import { render } from '@testing-library/react';
+import type { DocLinksStart } from 'src/core/public';
describe('TelemetryManagementSectionComponent', () => {
const coreStart = coreMock.createStart();
+ const docLinks = {} as DocLinksStart['links'];
const coreSetup = coreMock.createSetup();
it('renders as expected', () => {
@@ -45,6 +47,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
)
).toMatchSnapshot();
@@ -78,6 +81,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
@@ -93,6 +97,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
toasts={coreStart.notifications.toasts}
isSecurityExampleEnabled={isSecurityExampleEnabled}
+ docLinks={docLinks}
/>
);
@@ -130,6 +135,7 @@ describe('TelemetryManagementSectionComponent', () => {
isSecurityExampleEnabled={isSecurityExampleEnabled}
enableSaving={true}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
try {
@@ -177,6 +183,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
try {
@@ -215,6 +222,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
try {
@@ -254,6 +262,7 @@ describe('TelemetryManagementSectionComponent', () => {
isSecurityExampleEnabled={isSecurityExampleEnabled}
enableSaving={true}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
try {
@@ -293,6 +302,7 @@ describe('TelemetryManagementSectionComponent', () => {
isSecurityExampleEnabled={isSecurityExampleEnabled}
enableSaving={true}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
@@ -332,6 +342,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
isSecurityExampleEnabled={isSecurityExampleEnabled}
toasts={coreStart.notifications.toasts}
+ docLinks={docLinks}
/>
);
try {
@@ -339,11 +350,12 @@ describe('TelemetryManagementSectionComponent', () => {
await expect(
toggleOptInComponent.prop('handleChange')()
).resolves.toBe(true);
- expect((component.state() as any).enabled).toBe(true);
+ // TODO: Fix `mountWithIntl` types in @kbn/test/jest to make testing easier
+ expect((component.state() as { enabled: boolean }).enabled).toBe(true);
await expect(
toggleOptInComponent.prop('handleChange')()
).resolves.toBe(true);
- expect((component.state() as any).enabled).toBe(false);
+ expect((component.state() as { enabled: boolean }).enabled).toBe(false);
telemetryService.setOptIn = jest.fn().mockRejectedValue(Error('test-error'));
await expect(
toggleOptInComponent.prop('handleChange')()
@@ -381,6 +393,7 @@ describe('TelemetryManagementSectionComponent', () => {
enableSaving={true}
toasts={coreStart.notifications.toasts}
isSecurityExampleEnabled={isSecurityExampleEnabled}
+ docLinks={docLinks}
/>
).html()
).toMatchSnapshot();
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx
index 5a9f3922c6caf..e9ddc4cf82dfc 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx
@@ -20,12 +20,12 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
+import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
+import type { DocLinksStart, ToastsStart } from 'src/core/public';
import { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants';
import { OptInExampleFlyout } from './opt_in_example_flyout';
import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout';
import { LazyField } from '../../../advanced_settings/public';
-import { ToastsStart } from '../../../../core/public';
import { TrackApplicationView } from '../../../usage_collection/public';
type TelemetryService = TelemetryPluginSetup['telemetryService'];
@@ -40,6 +40,7 @@ interface Props {
enableSaving: boolean;
query?: { text: string };
toasts: ToastsStart;
+ docLinks: DocLinksStart['links'];
}
interface State {
@@ -130,24 +131,26 @@ export class TelemetryManagementSection extends Component {
{this.maybeGetAppliesSettingMessage()}
diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
index cc38b1ec74b37..91881dffa52d7 100644
--- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
+++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx
@@ -8,10 +8,12 @@
import React, { lazy, Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
-import { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
-// It should be this but the types are way too vague in the AdvancedSettings plugin `Record`
-// type Props = Omit;
-type Props = any;
+import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public';
+import type TelemetryManagementSection from './telemetry_management_section';
+export type TelemetryManagementSectionWrapperProps = Omit<
+ TelemetryManagementSection['props'],
+ 'telemetryService' | 'showAppliesSettingMessage' | 'isSecurityExampleEnabled'
+>;
const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_management_section'));
@@ -19,7 +21,7 @@ export function telemetryManagementSectionWrapper(
telemetryService: TelemetryPluginSetup['telemetryService'],
shouldShowSecuritySolutionUsageExample: () => boolean
) {
- const TelemetryManagementSectionWrapper = (props: Props) => (
+ const TelemetryManagementSectionWrapper = (props: TelemetryManagementSectionWrapperProps) => (
}>
);
},
diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts
index 90e873388d22e..22c91ac0c038d 100644
--- a/src/plugins/usage_collection/server/collector/collector.ts
+++ b/src/plugins/usage_collection/server/collector/collector.ts
@@ -141,11 +141,11 @@ export type CollectorOptions<
});
export class Collector {
- public readonly extendFetchContext: CollectorOptionsFetchExtendedContext;
- public readonly type: CollectorOptions['type'];
- public readonly init?: CollectorOptions['init'];
- public readonly fetch: CollectorFetchMethod;
- public readonly isReady: CollectorOptions['isReady'];
+ public readonly extendFetchContext: CollectorOptionsFetchExtendedContext;
+ public readonly type: CollectorOptions['type'];
+ public readonly init?: CollectorOptions['init'];
+ public readonly fetch: CollectorFetchMethod;
+ public readonly isReady: CollectorOptions['isReady'];
/**
* @private Constructor of a Collector. It should be called via the CollectorSet factory methods: `makeStatsCollector` and `makeUsageCollector`
* @param log {@link Logger}
@@ -160,7 +160,9 @@ export class Collector {
isReady,
extendFetchContext = {},
...options
- }: CollectorOptions
+ }: // Any does not affect here, but needs to be set so it doesn't affect anything else down the line
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ CollectorOptions
) {
if (type === undefined) {
throw new Error('Collector must be instantiated with a options.type string property');
diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts
index 0ef9a27cf094c..5a617e2316dda 100644
--- a/src/plugins/usage_collection/server/collector/collector_set.test.ts
+++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts
@@ -13,7 +13,7 @@ import { UsageCollector } from './usage_collector';
import {
elasticsearchServiceMock,
loggingSystemMock,
- savedObjectsRepositoryMock,
+ savedObjectsClientMock,
} from '../../../../core/server/mocks';
const logger = loggingSystemMock.createLogger();
@@ -34,7 +34,7 @@ describe('CollectorSet', () => {
loggerSpies.warn.mockRestore();
});
const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- const mockSoClient = savedObjectsRepositoryMock.create();
+ const mockSoClient = savedObjectsClientMock.create();
const req = void 0; // No need to instantiate any KibanaRequest in these tests
it('should throw an error if non-Collector type of object is registered', () => {
@@ -43,8 +43,9 @@ describe('CollectorSet', () => {
collectors.registerCollector({
type: 'type_collector_test',
init,
+ // @ts-expect-error we are intentionally sending it wrong.
fetch,
- } as any); // We are intentionally sending it wrong.
+ });
};
expect(registerPojo).toThrowError(
@@ -71,13 +72,14 @@ describe('CollectorSet', () => {
});
it('should log debug status of fetching from the collector', async () => {
- mockEsClient.get.mockResolvedValue({ passTest: 1000 } as any);
+ // @ts-expect-error we are just mocking the output of any call
+ mockEsClient.ping.mockResolvedValue({ passTest: 1000 });
const collectors = new CollectorSet({ logger });
collectors.registerCollector(
new Collector(logger, {
type: 'MY_TEST_COLLECTOR',
- fetch: (collectorFetchContext: any) => {
- return collectorFetchContext.esClient.get();
+ fetch: (collectorFetchContext) => {
+ return collectorFetchContext.esClient.ping();
},
isReady: () => true,
})
@@ -122,7 +124,8 @@ describe('CollectorSet', () => {
new Collector(logger, {
type: 'MY_TEST_COLLECTOR',
fetch: () => ({ test: 1 }),
- isReady: true as any,
+ // @ts-expect-error we are intentionally sending it wrong
+ isReady: true,
})
);
@@ -138,10 +141,11 @@ describe('CollectorSet', () => {
it('should not break if isReady is not provided', async () => {
const collectors = new CollectorSet({ logger });
collectors.registerCollector(
+ // @ts-expect-error we are intentionally sending it wrong.
new Collector(logger, {
type: 'MY_TEST_COLLECTOR',
fetch: () => ({ test: 1 }),
- } as any)
+ })
);
const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req);
diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts
index 4de5691eaaa70..d42eb6644bbbe 100644
--- a/src/plugins/usage_collection/server/collector/collector_set.ts
+++ b/src/plugins/usage_collection/server/collector/collector_set.ts
@@ -7,16 +7,17 @@
*/
import { snakeCase } from 'lodash';
-import {
+import type {
Logger,
ElasticsearchClient,
- ISavedObjectsRepository,
SavedObjectsClientContract,
KibanaRequest,
} from 'src/core/server';
import { Collector, CollectorOptions } from './collector';
import { UsageCollector, UsageCollectorOptions } from './usage_collector';
+// Needed for the general array containing all the collectors. We don't really care about their types here
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyCollector = Collector;
interface CollectorSetConfig {
@@ -144,7 +145,7 @@ export class CollectorSet {
public bulkFetch = async (
esClient: ElasticsearchClient,
- soClient: SavedObjectsClientContract | ISavedObjectsRepository,
+ soClient: SavedObjectsClientContract,
kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter
collectors: Map = this.collectors
) => {
@@ -183,7 +184,7 @@ export class CollectorSet {
public bulkFetchUsage = async (
esClient: ElasticsearchClient,
- savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository,
+ savedObjectsClient: SavedObjectsClientContract,
kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter
) => {
const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector);
diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts
index 1509b10654f49..3af3a7bb65f84 100644
--- a/src/plugins/usage_collection/server/collector/usage_collector.ts
+++ b/src/plugins/usage_collection/server/collector/usage_collector.ts
@@ -23,6 +23,8 @@ export class UsageCollector exte
> {
constructor(
log: Logger,
+ // Needed because it doesn't affect on anything here but being explicit creates a lot of pain down the line
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
collectorOptions: UsageCollectorOptions
) {
super(log, collectorOptions);
diff --git a/src/plugins/usage_collection/server/report/store_report.test.ts b/src/plugins/usage_collection/server/report/store_report.test.ts
index 08fdec4ae804f..789e01020bb2e 100644
--- a/src/plugins/usage_collection/server/report/store_report.test.ts
+++ b/src/plugins/usage_collection/server/report/store_report.test.ts
@@ -118,7 +118,7 @@ describe('store_report', () => {
expect(storeApplicationUsageMock).toHaveBeenCalledTimes(1);
expect(storeApplicationUsageMock).toHaveBeenCalledWith(
repository,
- Object.values(report.application_usage as Record),
+ Object.values(report.application_usage!),
expect.any(Date)
);
});
diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts
index 416a69dd9a8f9..6cae56afa281b 100644
--- a/src/plugins/usage_collection/server/routes/stats/stats.ts
+++ b/src/plugins/usage_collection/server/routes/stats/stats.ts
@@ -15,7 +15,6 @@ import { first } from 'rxjs/operators';
import {
ElasticsearchClient,
IRouter,
- ISavedObjectsRepository,
KibanaRequest,
MetricsServiceSetup,
SavedObjectsClientContract,
@@ -30,6 +29,12 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('usageCollection.stats.notReadyMe
const SNAPSHOT_REGEX = /-snapshot/i;
+interface UsageObject {
+ kibana?: UsageObject;
+ xpack?: UsageObject;
+ [key: string]: unknown | UsageObject;
+}
+
export function registerStatsRoute({
router,
config,
@@ -55,9 +60,9 @@ export function registerStatsRoute({
}) {
const getUsage = async (
esClient: ElasticsearchClient,
- savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository,
+ savedObjectsClient: SavedObjectsClientContract,
kibanaRequest: KibanaRequest
- ): Promise => {
+ ): Promise => {
const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient, kibanaRequest);
return collectorSet.toObject(usage);
};
@@ -104,7 +109,7 @@ export function registerStatsRoute({
const usagePromise = shouldGetUsage
? getUsage(asCurrentUser, savedObjectsClient, req)
- : Promise.resolve({});
+ : Promise.resolve({});
const [usage, clusterUuid] = await Promise.all([
usagePromise,
getClusterUuid(asCurrentUser),
@@ -138,7 +143,7 @@ export function registerStatsRoute({
}
return accum;
- }, {} as any);
+ }, {} as UsageObject);
extended = {
usage: modifiedUsage,
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts
index 694b155632d04..bee6db57bee42 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts
@@ -24,7 +24,8 @@ describe('getLicenseFromLocalOrMaster', () => {
test('returns the license it fetches from Elasticsearch', async () => {
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
// The local fetch succeeds
- esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } } as any);
+ // @ts-expect-error it's enough to test with minimal payload
+ esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } });
const license = await getLicenseFromLocalOrMaster(esClient);
@@ -51,7 +52,8 @@ describe('getLicenseFromLocalOrMaster', () => {
// The local fetch fails
esClient.license.get.mockRejectedValueOnce(new Error('Something went terribly wrong'));
// The master fetch succeeds
- esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } } as any);
+ // @ts-expect-error it's enough to test with minimal payload
+ esClient.license.get.mockResolvedValue({ body: { license: { type: 'basic' } } });
const license = await getLicenseFromLocalOrMaster(esClient);
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts
index 6ddb10e825684..86d1554a4fd57 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts
@@ -8,6 +8,8 @@
import type { estypes } from '@elastic/elasticsearch';
import { coreMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { getStatsWithXpack } from './get_stats_with_xpack';
+import { SavedObjectsClient } from '../../../../../src/core/server';
+import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/server/mocks';
const kibana = {
kibana: {
@@ -50,10 +52,16 @@ const getContext = () => ({
logger: coreMock.createPluginInitializerContext().logger.get('test'),
});
-const mockUsageCollection = (kibanaUsage: Record = kibana) => ({
- bulkFetch: () => kibanaUsage,
- toObject: (data: any) => data,
-});
+const mockUsageCollection = (kibanaUsage: Record = kibana) => {
+ const usageCollectionMock = usageCollectionPluginMock.createSetupContract();
+ usageCollectionMock.bulkFetch.mockImplementation(async () =>
+ Object.entries(kibanaUsage).map(([type, result]) => ({ type, result }))
+ );
+ usageCollectionMock.toObject.mockImplementation((data) =>
+ Object.fromEntries((data || []).map(({ type, result }) => [type, result]))
+ );
+ return usageCollectionMock;
+};
/**
* Instantiate the esClient mock with the common requests
@@ -91,6 +99,9 @@ function mockEsClient() {
}
describe('Telemetry Collection: Get Aggregated Stats', () => {
+ const soClient = new SavedObjectsClient(
+ coreMock.createStart().savedObjects.createInternalRepository()
+ );
test('OSS-like telemetry (no license nor X-Pack telemetry)', async () => {
const esClient = mockEsClient();
// mock for xpack.usage should throw a 404 for this test
@@ -106,7 +117,9 @@ describe('Telemetry Collection: Get Aggregated Stats', () => {
{
esClient,
usageCollection,
- } as any,
+ soClient,
+ kibanaRequest: undefined,
+ },
context
);
stats.forEach((entry) => {
@@ -126,7 +139,9 @@ describe('Telemetry Collection: Get Aggregated Stats', () => {
{
esClient,
usageCollection,
- } as any,
+ soClient,
+ kibanaRequest: undefined,
+ },
context
);
stats.forEach((entry) => {
@@ -151,7 +166,9 @@ describe('Telemetry Collection: Get Aggregated Stats', () => {
{
esClient,
usageCollection,
- } as any,
+ soClient,
+ kibanaRequest: undefined,
+ },
context
);
stats.forEach((entry, index) => {
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts
index 30bcd19007c0d..3adc5bc9f2eac 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts
@@ -45,7 +45,9 @@ export const getStatsWithXpack: StatsGetter = async fu
})
.reduce((acc, stats) => {
// Concatenate the telemetry reported via monitoring as additional payloads instead of reporting it inside of stack_stats.kibana.plugins.monitoringTelemetry
- const monitoringTelemetry = stats.stack_stats.kibana?.plugins?.monitoringTelemetry?.stats;
+ const monitoringTelemetry = stats.stack_stats.kibana?.plugins?.monitoringTelemetry?.stats as
+ | TelemetryAggregatedStats[]
+ | undefined;
if (monitoringTelemetry) {
delete stats.stack_stats.kibana!.plugins.monitoringTelemetry;
}
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts
index 5fa7584879f07..481bf70a5f5a7 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.test.ts
@@ -6,13 +6,14 @@
*/
import { isClusterOptedIn } from './is_cluster_opted_in';
+import type { TelemetryAggregatedStats } from './get_stats_with_xpack';
-const createMockClusterUsage = (plugins: any) => {
+const createMockClusterUsage = (plugins: unknown) => {
return {
stack_stats: {
kibana: { plugins },
},
- };
+ } as TelemetryAggregatedStats;
};
describe('isClusterOptedIn', () => {
@@ -33,6 +34,7 @@ describe('isClusterOptedIn', () => {
});
it('returns true if kibana.plugins.telemetry does not exist', () => {
expect(isClusterOptedIn(createMockClusterUsage({}))).toBe(true);
+ // @ts-expect-error we want to test the logic anyway because this object may come from very dynamic requests that any-fy the code
expect(isClusterOptedIn({})).toBe(true);
expect(isClusterOptedIn(undefined)).toBe(true);
});
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts
index 4bc35a238152b..49876f3d5a981 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/is_cluster_opted_in.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
-export const isClusterOptedIn = (clusterUsage: any): boolean => {
+import type { TelemetryAggregatedStats } from './get_stats_with_xpack';
+
+export const isClusterOptedIn = (clusterUsage?: TelemetryAggregatedStats): boolean => {
return (
clusterUsage?.stack_stats?.kibana?.plugins?.telemetry?.opt_in_status === true ||
// If stack_stats.kibana.plugins.telemetry does not exist, assume opted-in for BWC
From cbd6f844cb02814ad345b1e7e5f52e53a873367f Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Tue, 27 Apr 2021 13:25:29 -0700
Subject: [PATCH 16/70] Fetch geojson using ems-client (#97908)
* Fetch geojson using ems-client
* Review feedback
---
package.json | 2 +-
.../ems_file_source/ems_file_source.tsx | 43 +++++++++----------
.../ems_autosuggest/ems_autosuggest.test.ts | 41 ++++++++----------
.../public/ems_autosuggest/ems_autosuggest.ts | 11 ++---
yarn.lock | 9 +---
5 files changed, 42 insertions(+), 64 deletions(-)
diff --git a/package.json b/package.json
index 0a23be6779a7d..9d42daa63ede8 100644
--- a/package.json
+++ b/package.json
@@ -380,7 +380,7 @@
"tar": "4.4.13",
"tinycolor2": "1.4.1",
"tinygradient": "0.4.3",
- "topojson-client": "3.0.0",
+ "topojson-client": "3.1.0",
"tree-kill": "^1.2.2",
"ts-easing": "^0.2.0",
"tslib": "^2.0.0",
diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx
index c19ded6c2593e..22b873a94d1f7 100644
--- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx
@@ -12,13 +12,8 @@ import { Adapters } from 'src/plugins/inspector/public';
import { FileLayer } from '@elastic/ems-client';
import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source';
import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source';
-import {
- SOURCE_TYPES,
- FIELD_ORIGIN,
- VECTOR_SHAPE_TYPE,
- FORMAT_TYPE,
-} from '../../../../common/constants';
-import { fetchGeoJson, getEmsFileLayers } from '../../../util';
+import { SOURCE_TYPES, FIELD_ORIGIN, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
+import { getEmsFileLayers } from '../../../util';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { UpdateSourceEditor } from './update_source_editor';
import { EMSFileField } from '../../fields/ems_file_field';
@@ -122,24 +117,26 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc
}
async getGeoJsonWithMeta(): Promise {
- const emsFileLayer = await this.getEMSFileLayer();
- const featureCollection = await fetchGeoJson(
- emsFileLayer.getDefaultFormatUrl(),
- emsFileLayer.getDefaultFormatType() as FORMAT_TYPE,
- 'data'
- );
+ try {
+ const emsFileLayer = await this.getEMSFileLayer();
+ const featureCollection = await emsFileLayer.getGeoJson();
- const emsIdField = emsFileLayer.getFields().find((field) => {
- return field.type === 'id';
- });
- featureCollection.features.forEach((feature: Feature, index: number) => {
- feature.id = emsIdField ? feature!.properties![emsIdField.id] : index;
- });
+ if (!featureCollection) throw new Error('No features found');
- return {
- data: featureCollection,
- meta: {},
- };
+ const emsIdField = emsFileLayer.getFields().find((field) => {
+ return field.type === 'id';
+ });
+ featureCollection.features.forEach((feature: Feature, index: number) => {
+ feature.id = emsIdField ? feature!.properties![emsIdField.id] : index;
+ });
+
+ return {
+ data: featureCollection,
+ meta: {},
+ };
+ } catch (error) {
+ throw new Error(`${getErrorInfo(this._descriptor.id)} - ${error.message}`);
+ }
}
async getImmutableProperties(): Promise {
diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
index 34a53be48a5cd..eff49c1b1242e 100644
--- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
+++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts
@@ -6,7 +6,6 @@
*/
import { suggestEMSTermJoinConfig } from './ems_autosuggest';
-import { FORMAT_TYPE } from '../../common';
import { FeatureCollection } from 'geojson';
class MockFileLayer {
@@ -19,16 +18,28 @@ class MockFileLayer {
this._id = url;
this._fields = fields;
}
- getDefaultFormatUrl() {
- return this._url;
- }
getFields() {
return this._fields;
}
- getDefaultFormatType() {
- return FORMAT_TYPE.GEOJSON;
+ getGeoJson() {
+ if (this._url === 'world_countries') {
+ return ({
+ type: 'FeatureCollection',
+ features: [
+ { properties: { iso2: 'CA', iso3: 'CAN' } },
+ { properties: { iso2: 'US', iso3: 'USA' } },
+ ],
+ } as unknown) as FeatureCollection;
+ } else if (this._url === 'zips') {
+ return ({
+ type: 'FeatureCollection',
+ features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }],
+ } as unknown) as FeatureCollection;
+ } else {
+ throw new Error(`unrecognized mock url ${this._url}`);
+ }
}
hasId(id: string) {
@@ -44,24 +55,6 @@ jest.mock('../util', () => {
new MockFileLayer('zips', [{ id: 'zip' }]),
];
},
- async fetchGeoJson(url: string): Promise {
- if (url === 'world_countries') {
- return ({
- type: 'FeatureCollection',
- features: [
- { properties: { iso2: 'CA', iso3: 'CAN' } },
- { properties: { iso2: 'US', iso3: 'USA' } },
- ],
- } as unknown) as FeatureCollection;
- } else if (url === 'zips') {
- return ({
- type: 'FeatureCollection',
- features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }],
- } as unknown) as FeatureCollection;
- } else {
- throw new Error(`unrecognized mock url ${url}`);
- }
- },
};
});
diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
index 1d5c1529a004e..952e48a71a9dc 100644
--- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
+++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts
@@ -6,8 +6,8 @@
*/
import type { FileLayer } from '@elastic/ems-client';
-import { getEmsFileLayers, fetchGeoJson } from '../util';
-import { FORMAT_TYPE, emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common';
+import { getEmsFileLayers } from '../util';
+import { emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common';
export interface SampleValuesConfig {
emsLayerIds?: string[];
@@ -165,14 +165,9 @@ async function getMatchesForEMSLayer(
}
const emsFields = emsFileLayer.getFields();
- const url = emsFileLayer.getDefaultFormatUrl();
try {
- const emsJson = await fetchGeoJson(
- url,
- emsFileLayer.getDefaultFormatType() as FORMAT_TYPE,
- 'data'
- );
+ const emsJson = await emsFileLayer.getGeoJson();
const matches: EMSTermJoinConfig[] = [];
for (let f = 0; f < emsFields.length; f++) {
if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) {
diff --git a/yarn.lock b/yarn.lock
index 9302839004a33..4ba48bdeb8b6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -26843,14 +26843,7 @@ topo@3.x.x:
dependencies:
hoek "5.x.x"
-topojson-client@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.0.0.tgz#1f99293a77ef42a448d032a81aa982b73f360d2f"
- integrity sha1-H5kpOnfvQqRI0DKoGqmCtz82DS8=
- dependencies:
- commander "2"
-
-topojson-client@^3.1.0:
+topojson-client@3.1.0, topojson-client@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==
From 41b930c97c9aa6b4e1fdfd7946df2e5f11867202 Mon Sep 17 00:00:00 2001
From: Zacqary Adam Xeper
Date: Tue, 27 Apr 2021 15:39:13 -0500
Subject: [PATCH 17/70] [Fleet] Add troubleshooting link to setup instructions
(#98531)
* [Fleet] Add troubleshooting link to setup instructions
* Add troubleshooting link to Fleet Server instructions
---
.../enrollment_instructions/manual/index.tsx | 21 +++++++++++++++++++
.../fleet_server_requirement_page.tsx | 21 +++++++++++++++++++
2 files changed, 42 insertions(+)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx
index 0d4f067771be0..7a7e42b9d634f 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx
@@ -91,6 +91,27 @@ export const ManualInstructions: React.FunctionComponent = ({
}}
/>
+
+
+
+
+
+ ),
+ }}
+ />
+
>
);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx
index a068426ecd23a..3be5d864e80c8 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx
@@ -184,6 +184,27 @@ export const FleetServerCommandStep = ({
>
{installCommand}
+
+
+
+
+
+ ),
+ }}
+ />
+
>
) : null,
};
From 942243158d70afe4e9664ec1b5d8d84670ac282f Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Tue, 27 Apr 2021 14:42:39 -0600
Subject: [PATCH 18/70] [Maps] convert FeaturesTooltip to TS (#97920)
* [Maps] convert FeaturesTooltip to TS
* tslint
* pass addFilters to method to keep not null check closer to check for undefined
* FeaturesTooltip
* convert test to ts
* fix snapshot
* clean up cast
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/maps/common/constants.ts | 2 +-
.../elasticsearch_geo_utils.ts | 2 +-
.../vector/properties/style_property.ts | 2 +
....snap => feature_properties.test.tsx.snap} | 0
...oter.test.js.snap => footer.test.tsx.snap} | 0
...rm.js => feature_geometry_filter_form.tsx} | 51 +++++--
...es.test.js => feature_properties.test.tsx} | 36 +++--
...e_properties.js => feature_properties.tsx} | 137 +++++++++++++-----
...atures_tooltip.js => features_tooltip.tsx} | 101 ++++++++++---
.../{footer.test.js => footer.test.tsx} | 30 ++--
.../{footer.js => footer.tsx} | 39 +++--
11 files changed, 294 insertions(+), 106 deletions(-)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/{feature_properties.test.js.snap => feature_properties.test.tsx.snap} (100%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/{footer.test.js.snap => footer.test.tsx.snap} (100%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/{feature_geometry_filter_form.js => feature_geometry_filter_form.tsx} (68%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/{feature_properties.test.js => feature_properties.test.tsx} (72%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/{feature_properties.js => feature_properties.tsx} (65%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/{features_tooltip.js => features_tooltip.tsx} (66%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/{footer.test.js => footer.test.tsx} (89%)
rename x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/{footer.js => footer.tsx} (83%)
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index 007368f0997df..0d8930bdb75b8 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -295,7 +295,7 @@ export enum DATA_MAPPING_FUNCTION {
}
export const DEFAULT_PERCENTILES = [50, 75, 90, 95, 99];
-export type RawValue = string | number | boolean | undefined | null;
+export type RawValue = string | string[] | number | boolean | undefined | null;
export type FieldFormatter = (value: RawValue) => string | number;
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
index 197b7f49eda0a..c18a79fa9dcbc 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
@@ -371,7 +371,7 @@ export function createSpatialFilterWithGeometry({
geoFieldName,
relation = ES_SPATIAL_RELATIONS.INTERSECTS,
}: {
- preIndexedShape?: PreIndexedShape;
+ preIndexedShape?: PreIndexedShape | null;
geometry: Polygon;
geometryLabel: string;
indexPatternId: string;
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts
index f77a73e531029..41877406f7489 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts
@@ -59,6 +59,8 @@ export class AbstractStyleProperty implements IStyleProperty {
return '';
} else if (typeof value === 'boolean') {
return value.toString();
+ } else if (Array.isArray(value)) {
+ return value.join(', ');
} else {
return value;
}
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/feature_properties.test.js.snap b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/feature_properties.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/feature_properties.test.js.snap
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/feature_properties.test.tsx.snap
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/footer.test.js.snap b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/footer.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/footer.test.js.snap
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/__snapshots__/footer.test.tsx.snap
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.tsx
similarity index 68%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.tsx
index 9d4cf78c98754..61732d1c268c2 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_geometry_filter_form.tsx
@@ -8,19 +8,42 @@
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
+import { Filter } from 'src/plugins/data/public';
+import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
+import { Geometry, Polygon } from 'geojson';
+import rison, { RisonObject } from 'rison-node';
import { URL_MAX_LENGTH } from '../../../../../../../src/core/public';
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public';
-import { createSpatialFilterWithGeometry } from '../../../../common/elasticsearch_util';
-import { GEO_JSON_TYPE } from '../../../../common/constants';
+import {
+ createSpatialFilterWithGeometry,
+ PreIndexedShape,
+} from '../../../../common/elasticsearch_util';
+import { ES_SPATIAL_RELATIONS, GEO_JSON_TYPE } from '../../../../common/constants';
+// @ts-expect-error
import { GeometryFilterForm } from '../../../components/geometry_filter_form';
-
-import rison from 'rison-node';
+import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
// over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped.
const META_OVERHEAD = 100;
-export class FeatureGeometryFilterForm extends Component {
- state = {
+interface Props {
+ onClose: () => void;
+ geometry: Geometry;
+ geoFields: GeoFieldWithIndex[];
+ addFilters: (filters: Filter[], actionId: string) => Promise;
+ getFilterActions?: () => Promise;
+ getActionContext?: () => ActionExecutionContext;
+ loadPreIndexedShape: () => Promise;
+}
+
+interface State {
+ isLoading: boolean;
+ errorMsg: string | undefined;
+}
+
+export class FeatureGeometryFilterForm extends Component {
+ private _isMounted = false;
+ state: State = {
isLoading: false,
errorMsg: undefined,
};
@@ -52,7 +75,17 @@ export class FeatureGeometryFilterForm extends Component {
return preIndexedShape;
};
- _createFilter = async ({ geometryLabel, indexPatternId, geoFieldName, relation }) => {
+ _createFilter = async ({
+ geometryLabel,
+ indexPatternId,
+ geoFieldName,
+ relation,
+ }: {
+ geometryLabel: string;
+ indexPatternId: string;
+ geoFieldName: string;
+ relation: ES_SPATIAL_RELATIONS;
+ }) => {
this.setState({ errorMsg: undefined });
const preIndexedShape = await this._loadPreIndexedShape();
if (!this._isMounted) {
@@ -62,7 +95,7 @@ export class FeatureGeometryFilterForm extends Component {
const filter = createSpatialFilterWithGeometry({
preIndexedShape,
- geometry: this.props.geometry,
+ geometry: this.props.geometry as Polygon,
geometryLabel,
indexPatternId,
geoFieldName,
@@ -72,7 +105,7 @@ export class FeatureGeometryFilterForm extends Component {
// Ensure filter will not overflow URL. Filters that contain geometry can be extremely large.
// No elasticsearch support for pre-indexed shapes and geo_point spatial queries.
if (
- window.location.href.length + rison.encode(filter).length + META_OVERHEAD >
+ window.location.href.length + rison.encode(filter as RisonObject).length + META_OVERHEAD >
URL_MAX_LENGTH
) {
this.setState({
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.tsx
similarity index 72%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.tsx
index e7a2024afb98a..c999e9e6705cc 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.test.tsx
@@ -9,9 +9,15 @@ import React from 'react';
import { shallow } from 'enzyme';
import { FeatureProperties } from './feature_properties';
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public';
+import { ITooltipProperty } from '../../../classes/tooltips/tooltip_property';
+import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
class MockTooltipProperty {
- constructor(key, value, isFilterable) {
+ private _key: string;
+ private _value: string;
+ private _isFilterable: boolean;
+
+ constructor(key: string, value: string, isFilterable: boolean) {
this._key = key;
this._value = value;
this._isFilterable = isFilterable;
@@ -31,21 +37,27 @@ class MockTooltipProperty {
}
const defaultProps = {
- loadFeatureProperties: () => {
+ loadFeatureProperties: async () => {
return [];
},
featureId: `feature`,
layerId: `layer`,
+ mbProperties: {},
onCloseTooltip: () => {},
showFilterButtons: false,
- getFilterActions: () => {
- return [{ id: ACTION_GLOBAL_APPLY_FILTER }];
+ addFilters: async () => {},
+ getActionContext: () => {
+ return ({} as unknown) as ActionExecutionContext;
+ },
+ getFilterActions: async () => {
+ return [({ id: ACTION_GLOBAL_APPLY_FILTER } as unknown) as Action];
},
+ showFilterActions: () => {},
};
const mockTooltipProperties = [
- new MockTooltipProperty('prop1', 'foobar1', true),
- new MockTooltipProperty('prop2', 'foobar2', false),
+ (new MockTooltipProperty('prop1', 'foobar1', true) as unknown) as ITooltipProperty,
+ (new MockTooltipProperty('prop2', 'foobar2', false) as unknown) as ITooltipProperty,
];
describe('FeatureProperties', () => {
@@ -53,7 +65,7 @@ describe('FeatureProperties', () => {
const component = shallow(
{
+ loadFeatureProperties={async () => {
return mockTooltipProperties;
}}
/>
@@ -72,7 +84,7 @@ describe('FeatureProperties', () => {
{
+ loadFeatureProperties={async () => {
return mockTooltipProperties;
}}
/>
@@ -91,11 +103,11 @@ describe('FeatureProperties', () => {
{
+ loadFeatureProperties={async () => {
return mockTooltipProperties;
}}
- getFilterActions={() => {
- return [{ id: 'drilldown1' }];
+ getFilterActions={async () => {
+ return [({ id: 'drilldown1' } as unknown) as Action];
}}
/>
);
@@ -113,7 +125,7 @@ describe('FeatureProperties', () => {
{
+ loadFeatureProperties={async () => {
throw new Error('Simulated load properties error');
}}
/>
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.tsx
similarity index 65%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.tsx
index 2bd1d5c9cacf5..d221d4d5b1ca5 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/feature_properties.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { Component, CSSProperties, RefObject, ReactNode } from 'react';
import {
EuiCallOut,
EuiLoadingSpinner,
@@ -15,11 +15,51 @@ import {
EuiContextMenu,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
+import { GeoJsonProperties } from 'geojson';
+import { Filter } from 'src/plugins/data/public';
import { ACTION_GLOBAL_APPLY_FILTER } from '../../../../../../../src/plugins/data/public';
import { isUrlDrilldown } from '../../../trigger_actions/trigger_utils';
+import { RawValue } from '../../../../common/constants';
+import { ITooltipProperty } from '../../../classes/tooltips/tooltip_property';
-export class FeatureProperties extends React.Component {
- state = {
+interface Props {
+ featureId?: string | number;
+ layerId: string;
+ mbProperties: GeoJsonProperties;
+ loadFeatureProperties: ({
+ layerId,
+ featureId,
+ mbProperties,
+ }: {
+ layerId: string;
+ featureId?: string | number;
+ mbProperties: GeoJsonProperties;
+ }) => Promise;
+ showFilterButtons: boolean;
+ onCloseTooltip: () => void;
+ addFilters: ((filters: Filter[], actionId: string) => Promise) | null;
+ getFilterActions?: () => Promise;
+ getActionContext?: () => ActionExecutionContext;
+ onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
+ showFilterActions: (view: ReactNode) => void;
+}
+
+interface State {
+ properties: ITooltipProperty[] | null;
+ actions: Action[];
+ loadPropertiesErrorMsg: string | null;
+ prevWidth: number | null;
+ prevHeight: number | null;
+}
+
+export class FeatureProperties extends Component {
+ private _isMounted = false;
+ private _prevLayerId: string = '';
+ private _prevFeatureId?: string | number = '';
+ private readonly _tableRef: RefObject = React.createRef();
+
+ state: State = {
properties: null,
actions: [],
loadPropertiesErrorMsg: null,
@@ -29,8 +69,6 @@ export class FeatureProperties extends React.Component {
componentDidMount() {
this._isMounted = true;
- this.prevLayerId = undefined;
- this.prevFeatureId = undefined;
this._loadProperties();
this._loadActions();
}
@@ -61,28 +99,42 @@ export class FeatureProperties extends React.Component {
});
};
- _showFilterActions = (tooltipProperty) => {
- this.props.showFilterActions(this._renderFilterActions(tooltipProperty));
+ _showFilterActions = (
+ tooltipProperty: ITooltipProperty,
+ getActionContext: () => ActionExecutionContext,
+ addFilters: (filters: Filter[], actionId: string) => Promise
+ ) => {
+ this.props.showFilterActions(
+ this._renderFilterActions(tooltipProperty, getActionContext, addFilters)
+ );
};
- _fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => {
- if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) {
+ _fetchProperties = async ({
+ nextLayerId,
+ nextFeatureId,
+ mbProperties,
+ }: {
+ nextLayerId: string;
+ nextFeatureId?: string | number;
+ mbProperties: GeoJsonProperties;
+ }) => {
+ if (this._prevLayerId === nextLayerId && this._prevFeatureId === nextFeatureId) {
// do not reload same feature properties
return;
}
- this.prevLayerId = nextLayerId;
- this.prevFeatureId = nextFeatureId;
+ this._prevLayerId = nextLayerId;
+ this._prevFeatureId = nextFeatureId;
this.setState({
- properties: undefined,
- loadPropertiesErrorMsg: undefined,
+ properties: null,
+ loadPropertiesErrorMsg: null,
});
// Preserve current properties width/height so they can be used while rendering loading indicator.
- if (this.state.properties && this._node) {
+ if (this.state.properties && this._tableRef.current) {
this.setState({
- prevWidth: this._node.clientWidth,
- prevHeight: this._node.clientHeight,
+ prevWidth: this._tableRef.current.clientWidth,
+ prevHeight: this._tableRef.current.clientHeight,
});
}
@@ -91,7 +143,7 @@ export class FeatureProperties extends React.Component {
properties = await this.props.loadFeatureProperties({
layerId: nextLayerId,
featureId: nextFeatureId,
- mbProperties: mbProperties,
+ mbProperties,
});
} catch (error) {
if (this._isMounted) {
@@ -103,7 +155,7 @@ export class FeatureProperties extends React.Component {
return;
}
- if (this.prevLayerId !== nextLayerId && this.prevFeatureId !== nextFeatureId) {
+ if (this._prevLayerId !== nextLayerId && this._prevFeatureId !== nextFeatureId) {
// ignore results for old request
return;
}
@@ -113,7 +165,11 @@ export class FeatureProperties extends React.Component {
}
};
- _renderFilterActions(tooltipProperty) {
+ _renderFilterActions(
+ tooltipProperty: ITooltipProperty,
+ getActionContext: () => ActionExecutionContext,
+ addFilters: (filters: Filter[], actionId: string) => Promise
+ ) {
const panel = {
id: 0,
items: this.state.actions
@@ -124,24 +180,24 @@ export class FeatureProperties extends React.Component {
return true;
})
.map((action) => {
- const actionContext = this.props.getActionContext();
+ const actionContext = getActionContext();
const iconType = action.getIconType(actionContext);
const name = action.getDisplayName(actionContext);
return {
name: name ? name : action.id,
- icon: iconType ? : null,
+ icon: iconType ? : undefined,
onClick: async () => {
this.props.onCloseTooltip();
if (isUrlDrilldown(action)) {
- this.props.onSingleValueTrigger(
+ this.props.onSingleValueTrigger!(
action.id,
tooltipProperty.getPropertyKey(),
tooltipProperty.getRawValue()
);
} else {
const filters = await tooltipProperty.getESFilters();
- this.props.addFilters(filters, action.id);
+ addFilters(filters, action.id);
}
},
['data-test-subj']: `mapFilterActionButton__${name}`,
@@ -151,10 +207,7 @@ export class FeatureProperties extends React.Component {
return (
-
(this._node = node)}
- >
+
@@ -168,7 +221,7 @@ export class FeatureProperties extends React.Component {
* Since these formatters produce raw HTML, this component needs to be able to render them as-is, relying
* on the field formatter to only produce safe HTML.
*/
- dangerouslySetInnerHTML={{ __html: tooltipProperty.getHtmlDisplayValue() }} //eslint-disable-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: tooltipProperty.getHtmlDisplayValue() }} // eslint-disable-line react/no-danger
/>
@@ -178,8 +231,12 @@ export class FeatureProperties extends React.Component {
);
}
- _renderFilterCell(tooltipProperty) {
- if (!this.props.showFilterButtons || !tooltipProperty.isFilterable()) {
+ _renderFilterCell(tooltipProperty: ITooltipProperty) {
+ if (
+ !this.props.showFilterButtons ||
+ !tooltipProperty.isFilterable() ||
+ this.props.addFilters === undefined
+ ) {
return ;
}
@@ -192,7 +249,7 @@ export class FeatureProperties extends React.Component {
onClick={async () => {
this.props.onCloseTooltip();
const filters = await tooltipProperty.getESFilters();
- this.props.addFilters(filters, ACTION_GLOBAL_APPLY_FILTER);
+ this.props.addFilters!(filters, ACTION_GLOBAL_APPLY_FILTER);
}}
aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', {
defaultMessage: 'Filter on property',
@@ -203,7 +260,8 @@ export class FeatureProperties extends React.Component {
);
- return this.state.actions.length === 0 ||
+ return this.props.getActionContext === undefined ||
+ this.state.actions.length === 0 ||
(this.state.actions.length === 1 &&
this.state.actions[0].id === ACTION_GLOBAL_APPLY_FILTER) ? (
{applyFilterButton}
@@ -217,7 +275,11 @@ export class FeatureProperties extends React.Component {
defaultMessage: 'View filter actions',
})}
onClick={() => {
- this._showFilterActions(tooltipProperty);
+ this._showFilterActions(
+ tooltipProperty,
+ this.props.getActionContext!,
+ this.props.addFilters!
+ );
}}
aria-label={i18n.translate('xpack.maps.tooltip.viewActionsTitle', {
defaultMessage: 'View filter actions',
@@ -253,7 +315,7 @@ export class FeatureProperties extends React.Component {
});
// Use width/height of last viewed properties while displaying loading status
// to avoid resizing component during loading phase and bouncing tooltip container around
- const style = {};
+ const style: CSSProperties = {};
if (this.state.prevWidth && this.state.prevHeight) {
style.width = this.state.prevWidth;
style.height = this.state.prevHeight;
@@ -279,7 +341,7 @@ export class FeatureProperties extends React.Component {
* Since these formatters produce raw HTML, this component needs to be able to render them as-is, relying
* on the field formatter to only produce safe HTML.
*/
- dangerouslySetInnerHTML={{ __html: tooltipProperty.getHtmlDisplayValue() }} //eslint-disable-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: tooltipProperty.getHtmlDisplayValue() }} // eslint-disable-line react/no-danger
/>
{this._renderFilterCell(tooltipProperty)}
@@ -287,10 +349,7 @@ export class FeatureProperties extends React.Component {
});
return (
- (this._node = node)}
- >
+
);
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx
similarity index 66%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx
index be8e960471efa..41a2b98ab4b28 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/features_tooltip.tsx
@@ -5,26 +5,82 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component, Fragment, ReactNode } from 'react';
import { EuiIcon, EuiLink } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public';
+import { GeoJsonProperties, Geometry } from 'geojson';
+import { Filter } from 'src/plugins/data/public';
import { FeatureProperties } from './feature_properties';
-import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants';
+import { GEO_JSON_TYPE, ES_GEO_FIELD_TYPE, RawValue } from '../../../../common/constants';
import { FeatureGeometryFilterForm } from './feature_geometry_filter_form';
import { Footer } from './footer';
import { Header } from './header';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
+import { PreIndexedShape } from '../../../../common/elasticsearch_util';
+import { GeoFieldWithIndex } from '../../../components/geo_field_with_index';
+import { TooltipFeature } from '../../../../common/descriptor_types';
+import { ITooltipProperty } from '../../../classes/tooltips/tooltip_property';
+import { ILayer } from '../../../classes/layers/layer';
-const VIEWS = {
- PROPERTIES_VIEW: 'PROPERTIES_VIEW',
- GEOMETRY_FILTER_VIEW: 'GEOMETRY_FILTER_VIEW',
- FILTER_ACTIONS_VIEW: 'FILTER_ACTIONS_VIEW',
-};
+enum VIEWS {
+ PROPERTIES_VIEW = 'PROPERTIES_VIEW',
+ GEOMETRY_FILTER_VIEW = 'GEOMETRY_FILTER_VIEW',
+ FILTER_ACTIONS_VIEW = 'FILTER_ACTIONS_VIEW',
+}
-export class FeaturesTooltip extends Component {
- state = {};
+interface Props {
+ addFilters: ((filters: Filter[], actionId: string) => Promise) | null;
+ getFilterActions?: () => Promise;
+ getActionContext?: () => ActionExecutionContext;
+ onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void;
+ closeTooltip: () => void;
+ features: TooltipFeature[];
+ isLocked: boolean;
+ loadFeatureProperties: ({
+ layerId,
+ featureId,
+ mbProperties,
+ }: {
+ layerId: string;
+ featureId?: string | number;
+ mbProperties: GeoJsonProperties;
+ }) => Promise;
+ loadFeatureGeometry: ({
+ layerId,
+ featureId,
+ }: {
+ layerId: string;
+ featureId?: string | number;
+ }) => Geometry | null;
+ getLayerName: (layerId: string) => Promise;
+ findLayerById: (layerId: string) => ILayer | undefined;
+ geoFields: GeoFieldWithIndex[];
+ loadPreIndexedShape: ({
+ layerId,
+ featureId,
+ }: {
+ layerId: string;
+ featureId?: string | number;
+ }) => Promise;
+}
- static getDerivedStateFromProps(nextProps, prevState) {
+interface State {
+ currentFeature: TooltipFeature | null;
+ filterView: ReactNode | null;
+ prevFeatures: TooltipFeature[];
+ view: VIEWS;
+}
+
+export class FeaturesTooltip extends Component {
+ state: State = {
+ currentFeature: null,
+ filterView: null,
+ prevFeatures: [],
+ view: VIEWS.PROPERTIES_VIEW,
+ };
+
+ static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (nextProps.features !== prevState.prevFeatures) {
return {
currentFeature: nextProps.features ? nextProps.features[0] : null,
@@ -36,7 +92,7 @@ export class FeaturesTooltip extends Component {
return null;
}
- _setCurrentFeature = (feature) => {
+ _setCurrentFeature = (feature: TooltipFeature) => {
this.setState({ currentFeature: feature });
};
@@ -48,11 +104,11 @@ export class FeaturesTooltip extends Component {
this.setState({ view: VIEWS.PROPERTIES_VIEW, filterView: null });
};
- _showFilterActionsView = (filterView) => {
+ _showFilterActionsView = (filterView: ReactNode) => {
this.setState({ view: VIEWS.FILTER_ACTIONS_VIEW, filterView });
};
- _renderActions(geoFields) {
+ _renderActions(geoFields: GeoFieldWithIndex[]) {
if (!this.props.isLocked || geoFields.length === 0) {
return null;
}
@@ -67,7 +123,7 @@ export class FeaturesTooltip extends Component {
);
}
- _filterGeoFields(featureGeometry) {
+ _filterGeoFields(featureGeometry: Geometry | null) {
if (!featureGeometry) {
return [];
}
@@ -93,9 +149,9 @@ export class FeaturesTooltip extends Component {
return this.props.geoFields;
}
- _loadCurrentFeaturePreIndexedShape = () => {
+ _loadCurrentFeaturePreIndexedShape = async () => {
if (!this.state.currentFeature) {
- return;
+ return null;
}
return this.props.loadPreIndexedShape({
@@ -104,7 +160,7 @@ export class FeaturesTooltip extends Component {
});
};
- _renderBackButton(label) {
+ _renderBackButton(label: string) {
return (
{this._renderBackButton(
@@ -141,7 +201,6 @@ export class FeaturesTooltip extends Component {
)}
{
- return new MockLayer(id);
+ findLayerById: (id: string) => {
+ return ({
+ async getDisplayName() {
+ return `display + ${id}`;
+ },
+ getId() {
+ return id;
+ },
+ } as unknown) as ILayer;
},
setCurrentFeature: () => {},
};
@@ -35,6 +31,7 @@ describe('Footer', () => {
{
id: 'feature1',
layerId: 'layer1',
+ mbProperties: {},
},
];
describe('mouseover (unlocked)', () => {
@@ -56,10 +53,12 @@ describe('Footer', () => {
{
id: 'feature1',
layerId: 'layer1',
+ mbProperties: {},
},
{
id: 'feature2',
layerId: 'layer1',
+ mbProperties: {},
},
];
describe('mouseover (unlocked)', () => {
@@ -97,14 +96,17 @@ describe('Footer', () => {
{
id: 'feature1',
layerId: 'layer1',
+ mbProperties: {},
},
{
id: 'feature2',
layerId: 'layer1',
+ mbProperties: {},
},
{
id: 'feature1',
layerId: 'layer2',
+ mbProperties: {},
},
];
describe('mouseover (unlocked)', () => {
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/footer.js b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/footer.tsx
similarity index 83%
rename from x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/footer.js
rename to x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/footer.tsx
index 559e3fb18c182..3ad19a7901b09 100644
--- a/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/footer.js
+++ b/x-pack/plugins/maps/public/connected_components/mb_map/features_tooltip/footer.tsx
@@ -5,10 +5,11 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { ChangeEvent, Component, Fragment } from 'react';
import {
EuiPagination,
EuiSelect,
+ EuiSelectOption,
EuiHorizontalRule,
EuiFlexGroup,
EuiFlexItem,
@@ -17,12 +18,30 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { TooltipFeature } from '../../../../common/descriptor_types';
+import { ILayer } from '../../../classes/layers/layer';
const ALL_LAYERS = '_ALL_LAYERS_';
const DEFAULT_PAGE_NUMBER = 0;
-export class Footer extends Component {
- state = {
+interface Props {
+ features: TooltipFeature[];
+ isLocked: boolean;
+ findLayerById: (layerId: string) => ILayer | undefined;
+ setCurrentFeature: (feature: TooltipFeature) => void;
+}
+
+interface State {
+ filteredFeatures: TooltipFeature[];
+ pageNumber: number;
+ selectedLayerId: string;
+ layerOptions: EuiSelectOption[];
+}
+
+export class Footer extends Component {
+ private _isMounted = false;
+ private _prevFeatures: TooltipFeature[] | null = null;
+ state: State = {
filteredFeatures: this.props.features,
pageNumber: DEFAULT_PAGE_NUMBER,
selectedLayerId: ALL_LAYERS,
@@ -31,7 +50,6 @@ export class Footer extends Component {
componentDidMount() {
this._isMounted = true;
- this._prevFeatures = null;
this._loadUniqueLayers();
}
@@ -50,7 +68,7 @@ export class Footer extends Component {
this._prevFeatures = this.props.features;
- const countByLayerId = new Map();
+ const countByLayerId = new Map();
for (let i = 0; i < this.props.features.length; i++) {
let count = countByLayerId.get(this.props.features[i].layerId);
if (!count) {
@@ -60,9 +78,12 @@ export class Footer extends Component {
countByLayerId.set(this.props.features[i].layerId, count);
}
- const layers = [];
+ const layers: ILayer[] = [];
countByLayerId.forEach((count, layerId) => {
- layers.push(this.props.findLayerById(layerId));
+ const layer = this.props.findLayerById(layerId);
+ if (layer) {
+ layers.push(layer);
+ }
});
const layerNamePromises = layers.map((layer) => {
return layer.getDisplayName();
@@ -88,12 +109,12 @@ export class Footer extends Component {
}
};
- _onPageChange = (pageNumber) => {
+ _onPageChange = (pageNumber: number) => {
this.setState({ pageNumber });
this.props.setCurrentFeature(this.state.filteredFeatures[pageNumber]);
};
- _onLayerChange = (e) => {
+ _onLayerChange = (e: ChangeEvent) => {
const newLayerId = e.target.value;
if (this.state.selectedLayerId === newLayerId) {
return;
From 1a1c36a99bb395efeacdd1d1374f4bde1d2a018a Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Tue, 27 Apr 2021 16:50:39 -0400
Subject: [PATCH 19/70] Update best_practices.mdx (#98032)
---
dev_docs/best_practices.mdx | 1 +
1 file changed, 1 insertion(+)
diff --git a/dev_docs/best_practices.mdx b/dev_docs/best_practices.mdx
index 4d51263f93372..54aaaa6b9497a 100644
--- a/dev_docs/best_practices.mdx
+++ b/dev_docs/best_practices.mdx
@@ -65,6 +65,7 @@ Every publicly exposed function, class, interface, type, parameter and property
- Use `@returns` tags for return types.
- Use `@throws` when appropriate.
- Use `@beta` or `@deprecated` when appropriate.
+- Use `@removeBy {version}` on `@deprecated` APIs. The version should be the last version the API will work in. For example, `@removeBy 7.15` means the API will be removed in 7.16. This lets us avoid mid-release cycle coordination. The API can be removed as soon as the 7.15 branch is cut.
- Use `@internal` to indicate this API item is intended for internal use only, which will also remove it from the docs.
#### Interfaces vs inlined types
From 33f47ba59049dda7cb35c256ac33722219563928 Mon Sep 17 00:00:00 2001
From: Yuliia Naumenko
Date: Tue, 27 Apr 2021 14:14:01 -0700
Subject: [PATCH 20/70] [Connectors][API] Updated connectors with
isMissingSecrets flag (#98223)
* [Connectors][API] Updated connectors with enabledAfterImport flag
* fixed functional tests
* added new field to connectors API docs
* added update unit test
* fixed test
* renamed enableAfterImport to isMissingSecrets
* removed onExport
* revert the logic of true/false for isMissingSecrets
* fixed test
* fixed tests
* added unit test
* fixed docs
* fixed import text and button labels
* fixed import text
* fixed text
---
.../actions-and-connectors/create.asciidoc | 3 +-
docs/api/actions-and-connectors/get.asciidoc | 3 +-
.../actions-and-connectors/get_all.asciidoc | 1 +
.../legacy/create.asciidoc | 3 +-
.../legacy/get.asciidoc | 3 +-
.../legacy/get_all.asciidoc | 3 +-
.../legacy/update.asciidoc | 3 +-
.../actions-and-connectors/update.asciidoc | 3 +-
.../actions/server/actions_client.test.ts | 97 +++++++++++++++++++
.../plugins/actions/server/actions_client.ts | 5 +
.../actions/server/routes/create.test.ts | 6 +-
.../plugins/actions/server/routes/create.ts | 2 +
.../plugins/actions/server/routes/get.test.ts | 3 +
x-pack/plugins/actions/server/routes/get.ts | 2 +
.../plugins/actions/server/routes/get_all.ts | 15 +--
.../plugins/actions/server/routes/update.ts | 2 +
.../get_import_result_message.test.ts | 53 ++++++++++
.../get_import_result_message.ts | 25 +++++
.../actions/server/saved_objects/index.ts | 23 ++++-
.../server/saved_objects/mappings.json | 3 +
.../server/saved_objects/migrations.test.ts | 48 ++++++++-
.../server/saved_objects/migrations.ts | 18 ++++
x-pack/plugins/actions/server/types.ts | 2 +
.../actions/builtin_action_types/email.ts | 2 +
.../actions/builtin_action_types/es_index.ts | 4 +
.../actions/builtin_action_types/jira.ts | 2 +
.../actions/builtin_action_types/pagerduty.ts | 2 +
.../actions/builtin_action_types/resilient.ts | 2 +
.../builtin_action_types/server_log.ts | 2 +
.../builtin_action_types/servicenow.ts | 2 +
.../actions/builtin_action_types/slack.ts | 2 +
.../actions/builtin_action_types/webhook.ts | 4 +
.../tests/actions/create.ts | 1 +
.../security_and_spaces/tests/actions/get.ts | 1 +
.../tests/actions/get_all.ts | 2 +
.../tests/actions/update.ts | 1 +
.../actions/builtin_action_types/es_index.ts | 4 +
.../spaces_only/tests/actions/create.ts | 2 +
.../spaces_only/tests/actions/get.ts | 2 +
.../spaces_only/tests/actions/get_all.ts | 2 +
.../spaces_only/tests/actions/migrations.ts | 9 ++
.../tests/actions/type_not_enabled.ts | 2 +
.../spaces_only/tests/actions/update.ts | 2 +
.../basic/tests/configure/get_connectors.ts | 3 +
.../es_archives/actions/mappings.json | 3 +
45 files changed, 362 insertions(+), 20 deletions(-)
create mode 100644 x-pack/plugins/actions/server/saved_objects/get_import_result_message.test.ts
create mode 100644 x-pack/plugins/actions/server/saved_objects/get_import_result_message.ts
diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc
index 554e84615d568..c9ea31c98cf19 100644
--- a/docs/api/actions-and-connectors/create.asciidoc
+++ b/docs/api/actions-and-connectors/create.asciidoc
@@ -73,6 +73,7 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "is_preconfigured": false
+ "is_preconfigured": false,
+ "is_missing_secrets": false
}
--------------------------------------------------
diff --git a/docs/api/actions-and-connectors/get.asciidoc b/docs/api/actions-and-connectors/get.asciidoc
index 0d9af45c4ef0c..95336e7f55d30 100644
--- a/docs/api/actions-and-connectors/get.asciidoc
+++ b/docs/api/actions-and-connectors/get.asciidoc
@@ -50,6 +50,7 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "is_preconfigured": false
+ "is_preconfigured": false,
+ "is_missing_secrets": false
}
--------------------------------------------------
diff --git a/docs/api/actions-and-connectors/get_all.asciidoc b/docs/api/actions-and-connectors/get_all.asciidoc
index e4e67a9bbde73..8036b9fea7f95 100644
--- a/docs/api/actions-and-connectors/get_all.asciidoc
+++ b/docs/api/actions-and-connectors/get_all.asciidoc
@@ -56,6 +56,7 @@ The API returns the following:
"executionTimeField": null
},
"is_preconfigured": false,
+ "is_missing_secrets": false,
"referenced_by_count": 3
}
]
diff --git a/docs/api/actions-and-connectors/legacy/create.asciidoc b/docs/api/actions-and-connectors/legacy/create.asciidoc
index 0361c4222986b..e0d531a2befb9 100644
--- a/docs/api/actions-and-connectors/legacy/create.asciidoc
+++ b/docs/api/actions-and-connectors/legacy/create.asciidoc
@@ -75,6 +75,7 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "isPreconfigured": false
+ "isPreconfigured": false,
+ "isMissingSecrets": false
}
--------------------------------------------------
diff --git a/docs/api/actions-and-connectors/legacy/get.asciidoc b/docs/api/actions-and-connectors/legacy/get.asciidoc
index 6413fce558f5b..dab462e3ae4fb 100644
--- a/docs/api/actions-and-connectors/legacy/get.asciidoc
+++ b/docs/api/actions-and-connectors/legacy/get.asciidoc
@@ -52,6 +52,7 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "isPreconfigured": false
+ "isPreconfigured": false,
+ "isMissingSecrets": false
}
--------------------------------------------------
diff --git a/docs/api/actions-and-connectors/legacy/get_all.asciidoc b/docs/api/actions-and-connectors/legacy/get_all.asciidoc
index 191eccb6f8d39..2180720ce6542 100644
--- a/docs/api/actions-and-connectors/legacy/get_all.asciidoc
+++ b/docs/api/actions-and-connectors/legacy/get_all.asciidoc
@@ -56,7 +56,8 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "isPreconfigured": false
+ "isPreconfigured": false,
+ "isMissingSecrets": false
}
]
--------------------------------------------------
diff --git a/docs/api/actions-and-connectors/legacy/update.asciidoc b/docs/api/actions-and-connectors/legacy/update.asciidoc
index 6a33e765cf063..5202f8124e6a8 100644
--- a/docs/api/actions-and-connectors/legacy/update.asciidoc
+++ b/docs/api/actions-and-connectors/legacy/update.asciidoc
@@ -70,6 +70,7 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "isPreconfigured": false
+ "isPreconfigured": false,
+ "isMissingSecrets": false
}
--------------------------------------------------
diff --git a/docs/api/actions-and-connectors/update.asciidoc b/docs/api/actions-and-connectors/update.asciidoc
index f522cb8d048e0..0b7dcc898a122 100644
--- a/docs/api/actions-and-connectors/update.asciidoc
+++ b/docs/api/actions-and-connectors/update.asciidoc
@@ -68,6 +68,7 @@ The API returns the following:
"refresh": false,
"executionTimeField": null
},
- "is_preconfigured": false
+ "is_preconfigured": false,
+ "is_missing_secrets": false
}
--------------------------------------------------
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index 9b22e31c05e8a..30108a0777819 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -92,6 +92,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -123,6 +124,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -162,6 +164,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -199,6 +202,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -250,6 +254,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -274,6 +279,7 @@ describe('create()', () => {
isPreconfigured: false,
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
});
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
@@ -283,6 +289,7 @@ describe('create()', () => {
Object {
"actionTypeId": "my-action-type",
"config": Object {},
+ "isMissingSecrets": false,
"name": "my name",
"secrets": Object {},
},
@@ -347,6 +354,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {
a: true,
b: true,
@@ -373,6 +381,7 @@ describe('create()', () => {
isPreconfigured: false,
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {
a: true,
b: true,
@@ -390,6 +399,7 @@ describe('create()', () => {
"b": true,
"c": true,
},
+ "isMissingSecrets": false,
"name": "my name",
"secrets": Object {},
},
@@ -449,6 +459,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -482,6 +493,7 @@ describe('create()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -518,6 +530,7 @@ describe('get()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -566,6 +579,7 @@ describe('get()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -628,6 +642,7 @@ describe('get()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -653,6 +668,7 @@ describe('get()', () => {
attributes: {
name: 'my name',
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
config: {},
},
references: [],
@@ -821,6 +837,7 @@ describe('getAll()', () => {
type: 'type',
attributes: {
name: 'test',
+ isMissingSecrets: false,
config: {
foo: 'bar',
},
@@ -881,6 +898,7 @@ describe('getAll()', () => {
type: 'type',
attributes: {
name: 'test',
+ isMissingSecrets: false,
config: {
foo: 'bar',
},
@@ -932,6 +950,7 @@ describe('getAll()', () => {
config: {
foo: 'bar',
},
+ isMissingSecrets: false,
referencedByCount: 6,
},
{
@@ -959,6 +978,7 @@ describe('getBulk()', () => {
config: {
foo: 'bar',
},
+ isMissingSecrets: false,
},
references: [],
},
@@ -1030,6 +1050,7 @@ describe('getBulk()', () => {
config: {
foo: 'bar',
},
+ isMissingSecrets: false,
},
references: [],
},
@@ -1088,6 +1109,7 @@ describe('getBulk()', () => {
config: {
foo: 'bar',
},
+ isMissingSecrets: false,
},
references: [],
},
@@ -1143,6 +1165,7 @@ describe('getBulk()', () => {
foo: 'bar',
},
id: '1',
+ isMissingSecrets: false,
isPreconfigured: false,
name: 'test',
},
@@ -1231,6 +1254,7 @@ describe('update()', () => {
type: 'action',
attributes: {
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
},
references: [],
});
@@ -1239,6 +1263,7 @@ describe('update()', () => {
type: 'action',
attributes: {
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
name: 'my name',
config: {},
secrets: {},
@@ -1319,6 +1344,7 @@ describe('update()', () => {
type: 'action',
attributes: {
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
},
references: [],
});
@@ -1327,6 +1353,7 @@ describe('update()', () => {
type: 'action',
attributes: {
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
name: 'my name',
config: {},
secrets: {},
@@ -1345,6 +1372,7 @@ describe('update()', () => {
id: 'my-action',
isPreconfigured: false,
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
name: 'my name',
config: {},
});
@@ -1355,6 +1383,7 @@ describe('update()', () => {
Object {
"actionTypeId": "my-action-type",
"config": Object {},
+ "isMissingSecrets": false,
"name": "my name",
"secrets": Object {},
},
@@ -1374,6 +1403,70 @@ describe('update()', () => {
`);
});
+ test('updates an action with isMissingSecrets "true" (set true as the import result), to isMissingSecrets', async () => {
+ actionTypeRegistry.register({
+ id: 'my-action-type',
+ name: 'My action type',
+ minimumLicenseRequired: 'basic',
+ executor,
+ });
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
+ id: '1',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-type',
+ isMissingSecrets: true,
+ },
+ references: [],
+ });
+ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
+ id: 'my-action',
+ type: 'action',
+ attributes: {
+ actionTypeId: 'my-action-type',
+ isMissingSecrets: true,
+ name: 'my name',
+ config: {},
+ secrets: {},
+ },
+ references: [],
+ });
+ const result = await actionsClient.update({
+ id: 'my-action',
+ action: {
+ name: 'my name',
+ config: {},
+ secrets: {},
+ },
+ });
+ expect(result).toEqual({
+ id: 'my-action',
+ isPreconfigured: false,
+ actionTypeId: 'my-action-type',
+ isMissingSecrets: true,
+ name: 'my name',
+ config: {},
+ });
+ expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
+ expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "action",
+ Object {
+ "actionTypeId": "my-action-type",
+ "config": Object {},
+ "isMissingSecrets": false,
+ "name": "my name",
+ "secrets": Object {},
+ },
+ Object {
+ "id": "my-action",
+ "overwrite": true,
+ "references": Array [],
+ },
+ ]
+ `);
+ });
+
test('validates config', async () => {
actionTypeRegistry.register({
id: 'my-action-type',
@@ -1428,6 +1521,7 @@ describe('update()', () => {
type: 'action',
attributes: {
actionTypeId: 'my-action-type',
+ isMissingSecrets: true,
name: 'my name',
config: {
a: true,
@@ -1454,6 +1548,7 @@ describe('update()', () => {
id: 'my-action',
isPreconfigured: false,
actionTypeId: 'my-action-type',
+ isMissingSecrets: true,
name: 'my name',
config: {
a: true,
@@ -1472,6 +1567,7 @@ describe('update()', () => {
"b": true,
"c": true,
},
+ "isMissingSecrets": false,
"name": "my name",
"secrets": Object {},
},
@@ -1507,6 +1603,7 @@ describe('update()', () => {
type: 'action',
attributes: {
actionTypeId: 'my-action-type',
+ isMissingSecrets: false,
name: 'my name',
config: {},
secrets: {},
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index 9f87de5f686cc..c655141415b54 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -155,6 +155,7 @@ export class ActionsClient {
{
actionTypeId,
name,
+ isMissingSecrets: false,
config: validatedActionTypeConfig as SavedObjectAttributes,
secrets: validatedActionTypeSecrets as SavedObjectAttributes,
},
@@ -164,6 +165,7 @@ export class ActionsClient {
return {
id: result.id,
actionTypeId: result.attributes.actionTypeId,
+ isMissingSecrets: result.attributes.isMissingSecrets,
name: result.attributes.name,
config: result.attributes.config,
isPreconfigured: false,
@@ -228,6 +230,7 @@ export class ActionsClient {
...attributes,
actionTypeId,
name,
+ isMissingSecrets: false,
config: validatedActionTypeConfig as SavedObjectAttributes,
secrets: validatedActionTypeSecrets as SavedObjectAttributes,
},
@@ -245,6 +248,7 @@ export class ActionsClient {
return {
id,
actionTypeId: result.attributes.actionTypeId as string,
+ isMissingSecrets: result.attributes.isMissingSecrets as boolean,
name: result.attributes.name as string,
config: result.attributes.config as Record,
isPreconfigured: false,
@@ -299,6 +303,7 @@ export class ActionsClient {
return {
id,
actionTypeId: result.attributes.actionTypeId,
+ isMissingSecrets: result.attributes.isMissingSecrets,
name: result.attributes.name,
config: result.attributes.config,
isPreconfigured: false,
diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts
index e5d8e6f5861f3..51a55309b52ae 100644
--- a/x-pack/plugins/actions/server/routes/create.test.ts
+++ b/x-pack/plugins/actions/server/routes/create.test.ts
@@ -39,12 +39,14 @@ describe('createActionRoute', () => {
actionTypeId: 'abc',
config: { foo: true },
isPreconfigured: false,
+ isMissingSecrets: false,
};
const createApiResult = {
- ...omit(createResult, ['actionTypeId', 'isPreconfigured']),
+ ...omit(createResult, ['actionTypeId', 'isPreconfigured', 'isMissingSecrets']),
connector_type_id: createResult.actionTypeId,
is_preconfigured: createResult.isPreconfigured,
+ is_missing_secrets: createResult.isMissingSecrets,
};
const actionsClient = actionsClientMock.create();
@@ -99,6 +101,7 @@ describe('createActionRoute', () => {
id: '1',
name: 'My name',
actionTypeId: 'abc',
+ isMissingSecrets: false,
config: { foo: true },
isPreconfigured: false,
});
@@ -138,6 +141,7 @@ describe('createActionRoute', () => {
name: 'My name',
actionTypeId: 'abc',
config: { foo: true },
+ isMissingSecrets: false,
isPreconfigured: false,
});
diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts
index c05f2180bd62b..0c243b6a4eaa9 100644
--- a/x-pack/plugins/actions/server/routes/create.ts
+++ b/x-pack/plugins/actions/server/routes/create.ts
@@ -29,11 +29,13 @@ const rewriteBodyReq: RewriteRequestCase = ({
const rewriteBodyRes: RewriteResponseCase = ({
actionTypeId,
isPreconfigured,
+ isMissingSecrets,
...res
}) => ({
...res,
connector_type_id: actionTypeId,
is_preconfigured: isPreconfigured,
+ is_missing_secrets: isMissingSecrets,
});
export const createActionRoute = (
diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts
index 6a42f3b27370e..1107ec243bc01 100644
--- a/x-pack/plugins/actions/server/routes/get.test.ts
+++ b/x-pack/plugins/actions/server/routes/get.test.ts
@@ -38,6 +38,7 @@ describe('getActionRoute', () => {
name: 'action name',
config: {},
isPreconfigured: false,
+ isMissingSecrets: false,
};
const actionsClient = actionsClientMock.create();
@@ -57,6 +58,7 @@ describe('getActionRoute', () => {
"config": Object {},
"connector_type_id": "2",
"id": "1",
+ "is_missing_secrets": false,
"is_preconfigured": false,
"name": "action name",
},
@@ -73,6 +75,7 @@ describe('getActionRoute', () => {
name: 'action name',
config: {},
is_preconfigured: false,
+ is_missing_secrets: false,
},
});
});
diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts
index 59766fc133ba6..3f4a67c3bfbcd 100644
--- a/x-pack/plugins/actions/server/routes/get.ts
+++ b/x-pack/plugins/actions/server/routes/get.ts
@@ -19,11 +19,13 @@ const paramSchema = schema.object({
const rewriteBodyRes: RewriteResponseCase = ({
actionTypeId,
isPreconfigured,
+ isMissingSecrets,
...res
}) => ({
...res,
connector_type_id: actionTypeId,
is_preconfigured: isPreconfigured,
+ is_missing_secrets: isMissingSecrets,
});
export const getActionRoute = (
diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts
index 831722fd36eed..2d3a2727e9663 100644
--- a/x-pack/plugins/actions/server/routes/get_all.ts
+++ b/x-pack/plugins/actions/server/routes/get_all.ts
@@ -12,12 +12,15 @@ import { ActionsRequestHandlerContext, FindActionResult } from '../types';
import { verifyAccessAndContext } from './verify_access_and_context';
const rewriteBodyRes: RewriteResponseCase = (results) => {
- return results.map(({ actionTypeId, isPreconfigured, referencedByCount, ...res }) => ({
- ...res,
- connector_type_id: actionTypeId,
- is_preconfigured: isPreconfigured,
- referenced_by_count: referencedByCount,
- }));
+ return results.map(
+ ({ actionTypeId, isPreconfigured, referencedByCount, isMissingSecrets, ...res }) => ({
+ ...res,
+ connector_type_id: actionTypeId,
+ is_preconfigured: isPreconfigured,
+ referenced_by_count: referencedByCount,
+ is_missing_secrets: isMissingSecrets,
+ })
+ );
};
export const getAllActionRoute = (
diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts
index d1758717e80f9..276ce80751726 100644
--- a/x-pack/plugins/actions/server/routes/update.ts
+++ b/x-pack/plugins/actions/server/routes/update.ts
@@ -25,11 +25,13 @@ const bodySchema = schema.object({
const rewriteBodyRes: RewriteResponseCase = ({
actionTypeId,
isPreconfigured,
+ isMissingSecrets,
...res
}) => ({
...res,
connector_type_id: actionTypeId,
is_preconfigured: isPreconfigured,
+ is_missing_secrets: isMissingSecrets,
});
export const updateActionRoute = (
diff --git a/x-pack/plugins/actions/server/saved_objects/get_import_result_message.test.ts b/x-pack/plugins/actions/server/saved_objects/get_import_result_message.test.ts
new file mode 100644
index 0000000000000..b5a5ab75b9248
--- /dev/null
+++ b/x-pack/plugins/actions/server/saved_objects/get_import_result_message.test.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObject } from 'kibana/server';
+import { RawAction } from '../types';
+import { getImportResultMessage } from './get_import_result_message';
+
+describe('getImportResultMessage', () => {
+ it('Return message with total imported connectors and the proper secrets need to update ', async () => {
+ const savedObjectConnectors = [
+ {
+ type: 'action',
+ id: 'ed02cb70-a6ef-11eb-bd58-6b2eae02c6ef',
+ attributes: {
+ actionTypeId: '.server-log',
+ config: {},
+ isMissingSecrets: false,
+ name: 'test',
+ },
+ references: [],
+ migrationVersion: { action: '7.14.0' },
+ coreMigrationVersion: '8.0.0',
+ updated_at: '2021-04-27T04:10:33.043Z',
+ version: 'WzcxLDFd',
+ namespaces: ['default'],
+ },
+ {
+ type: 'action',
+ id: 'e8aa94e0-a6ef-11eb-bd58-6b2eae02c6ef',
+ attributes: {
+ actionTypeId: '.email',
+ config: [Object],
+ isMissingSecrets: true,
+ name: 'test',
+ },
+ references: [],
+ migrationVersion: { action: '7.14.0' },
+ coreMigrationVersion: '8.0.0',
+ updated_at: '2021-04-27T04:10:33.043Z',
+ version: 'WzcyLDFd',
+ namespaces: ['default'],
+ },
+ ];
+ const message = getImportResultMessage(
+ (savedObjectConnectors as unknown) as Array>
+ );
+ expect(message).toBe('1 connector has secrets that require updates.');
+ });
+});
diff --git a/x-pack/plugins/actions/server/saved_objects/get_import_result_message.ts b/x-pack/plugins/actions/server/saved_objects/get_import_result_message.ts
new file mode 100644
index 0000000000000..3b88a750c7430
--- /dev/null
+++ b/x-pack/plugins/actions/server/saved_objects/get_import_result_message.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { SavedObject } from 'kibana/server';
+import { RawAction } from '../types';
+
+export function getImportResultMessage(connectors: Array>) {
+ const connectorsWithSecrets = connectors.filter(
+ (connector) => connector.attributes.isMissingSecrets
+ );
+ return i18n.translate('xpack.actions.savedObjects.onImportText', {
+ defaultMessage:
+ '{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} secrets that require updates.',
+ values: {
+ connectorsWithSecretsLength: connectorsWithSecrets.length,
+ },
+ });
+}
+
+export const GO_TO_CONNECTORS_BUTTON_LABLE = 'Go to connectors';
diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts
index c8626660de2d9..3c6a78a6f0866 100644
--- a/x-pack/plugins/actions/server/saved_objects/index.ts
+++ b/x-pack/plugins/actions/server/saved_objects/index.ts
@@ -5,10 +5,12 @@
* 2.0.
*/
-import { SavedObjectsServiceSetup } from 'kibana/server';
+import { SavedObject, SavedObjectsServiceSetup } from 'kibana/server';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
import mappings from './mappings.json';
import { getMigrations } from './migrations';
+import { RawAction } from '../types';
+import { getImportResultMessage, GO_TO_CONNECTORS_BUTTON_LABLE } from './get_import_result_message';
export const ACTION_SAVED_OBJECT_TYPE = 'action';
export const ALERT_SAVED_OBJECT_TYPE = 'alert';
@@ -24,6 +26,25 @@ export function setupSavedObjects(
namespaceType: 'single',
mappings: mappings.action,
migrations: getMigrations(encryptedSavedObjects),
+ management: {
+ defaultSearchField: 'name',
+ importableAndExportable: true,
+ getTitle(obj) {
+ return `Connector: [${obj.attributes.name}]`;
+ },
+ onImport(connectors) {
+ return {
+ warnings: [
+ {
+ type: 'action_required',
+ message: getImportResultMessage(connectors as Array>),
+ actionPath: '/app/management/insightsAndAlerting/triggersActions/connectors',
+ buttonLabel: GO_TO_CONNECTORS_BUTTON_LABLE,
+ },
+ ],
+ };
+ },
+ },
});
// Encrypted attributes
diff --git a/x-pack/plugins/actions/server/saved_objects/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json
index ef6a0c9919920..c598b96ba2451 100644
--- a/x-pack/plugins/actions/server/saved_objects/mappings.json
+++ b/x-pack/plugins/actions/server/saved_objects/mappings.json
@@ -12,6 +12,9 @@
"actionTypeId": {
"type": "keyword"
},
+ "isMissingSecrets": {
+ "type": "boolean"
+ },
"config": {
"enabled": false,
"type": "object"
diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts
index a75735e514c10..4c30925e61894 100644
--- a/x-pack/plugins/actions/server/saved_objects/migrations.test.ts
+++ b/x-pack/plugins/actions/server/saved_objects/migrations.test.ts
@@ -43,7 +43,7 @@ describe('7.10.0', () => {
test('rename cases configuration object', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
- const action = getMockData({});
+ const action = getCasesMockData({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
incidentConfiguration: { mapping: [] },
@@ -112,10 +112,32 @@ describe('7.11.0', () => {
});
});
+describe('7.14.0', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ encryptedSavedObjectsSetup.createMigration.mockImplementation(
+ (shouldMigrateWhenPredicate, migration) => migration
+ );
+ });
+
+ test('add isMissingSecrets property for actions', () => {
+ const migration714 = getMigrations(encryptedSavedObjectsSetup)['7.14.0'];
+ const action = getMockData({ isMissingSecrets: undefined });
+ const migratedAction = migration714(action, context);
+ expect(migratedAction).toEqual({
+ ...action,
+ attributes: {
+ ...action.attributes,
+ isMissingSecrets: false,
+ },
+ });
+ });
+});
+
function getMockDataForWebhook(
overwrites: Record = {},
hasUserAndPassword: boolean
-): SavedObjectUnsanitizedDoc {
+): SavedObjectUnsanitizedDoc> {
const secrets = hasUserAndPassword
? { user: 'test', password: '123' }
: { user: '', password: '' };
@@ -134,7 +156,7 @@ function getMockDataForWebhook(
function getMockDataForEmail(
overwrites: Record = {}
-): SavedObjectUnsanitizedDoc {
+): SavedObjectUnsanitizedDoc> {
return {
attributes: {
name: 'abc',
@@ -148,9 +170,9 @@ function getMockDataForEmail(
};
}
-function getMockData(
+function getCasesMockData(
overwrites: Record = {}
-): SavedObjectUnsanitizedDoc {
+): SavedObjectUnsanitizedDoc> {
return {
attributes: {
name: 'abc',
@@ -163,3 +185,19 @@ function getMockData(
type: 'action',
};
}
+
+function getMockData(
+ overwrites: Record = {}
+): SavedObjectUnsanitizedDoc> {
+ return {
+ attributes: {
+ name: 'abc',
+ actionTypeId: '123',
+ config: {},
+ secrets: {},
+ ...overwrites,
+ },
+ id: uuid.v4(),
+ type: 'action',
+ };
+}
diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.ts b/x-pack/plugins/actions/server/saved_objects/migrations.ts
index 9bd54330f5d05..17932b6b90f97 100644
--- a/x-pack/plugins/actions/server/saved_objects/migrations.ts
+++ b/x-pack/plugins/actions/server/saved_objects/migrations.ts
@@ -41,9 +41,15 @@ export function getMigrations(
pipeMigrations(removeCasesFieldMappings, addHasAuthConfigurationObject)
);
+ const migrationActionsFourteen = encryptedSavedObjects.createMigration(
+ (doc): doc is SavedObjectUnsanitizedDoc => true,
+ pipeMigrations(addisMissingSecretsField)
+ );
+
return {
'7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'),
'7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'),
+ '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'),
};
}
@@ -127,6 +133,18 @@ const addHasAuthConfigurationObject = (
};
};
+const addisMissingSecretsField = (
+ doc: SavedObjectUnsanitizedDoc
+): SavedObjectUnsanitizedDoc => {
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ isMissingSecrets: false,
+ },
+ };
+};
+
function pipeMigrations(...migrations: ActionMigration[]): ActionMigration {
return (doc: SavedObjectUnsanitizedDoc) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index d6f99a766ed34..ea22e90dfed40 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -63,6 +63,7 @@ export interface ActionResult {
+ const responseWithisMissingSecrets = await supertest.get(
+ `${getUrlPrefix(``)}/api/actions/action/7434121e-045a-47d6-a0a6-0b6da752397a`
+ );
+
+ expect(responseWithisMissingSecrets.status).to.eql(200);
+ expect(responseWithisMissingSecrets.body.isMissingSecrets).to.eql(false);
+ });
});
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts
index 1461ddfaa83af..6969a7fb181e2 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts
@@ -62,6 +62,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext)
config: {},
id: 'uuid-actionId',
isPreconfigured: false,
+ isMissingSecrets: false,
name: 'an action created before test.not-enabled was disabled',
});
});
@@ -89,6 +90,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext)
config: {},
id: 'uuid-actionId',
isPreconfigured: false,
+ isMissingSecrets: false,
name: 'an action created before test.not-enabled was disabled',
});
});
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts
index 4e0b4eac8da32..f779a97eafe2e 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts
@@ -52,6 +52,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
id: createdAction.id,
is_preconfigured: false,
connector_type_id: 'test.index-record',
+ is_missing_secrets: false,
name: 'My action updated',
config: {
unencrypted: `This value shouldn't get encrypted`,
@@ -193,6 +194,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
id: createdAction.id,
isPreconfigured: false,
actionTypeId: 'test.index-record',
+ isMissingSecrets: false,
name: 'My action updated',
config: {
unencrypted: `This value shouldn't get encrypted`,
diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts
index 1789fa719ec9f..7eaa5ca609cf8 100644
--- a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts
+++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts
@@ -92,6 +92,7 @@ export default ({ getService }: FtrProviderContext): void => {
projectKey: 'pkey',
},
isPreconfigured: false,
+ isMissingSecrets: false,
referencedByCount: 0,
},
{
@@ -103,6 +104,7 @@ export default ({ getService }: FtrProviderContext): void => {
orgId: 'pkey',
},
isPreconfigured: false,
+ isMissingSecrets: false,
referencedByCount: 0,
},
{
@@ -113,6 +115,7 @@ export default ({ getService }: FtrProviderContext): void => {
apiUrl: 'http://some.non.existent.com',
},
isPreconfigured: false,
+ isMissingSecrets: false,
referencedByCount: 0,
},
]);
diff --git a/x-pack/test/functional/es_archives/actions/mappings.json b/x-pack/test/functional/es_archives/actions/mappings.json
index 7101af08400a2..737e0df57552e 100644
--- a/x-pack/test/functional/es_archives/actions/mappings.json
+++ b/x-pack/test/functional/es_archives/actions/mappings.json
@@ -87,6 +87,9 @@
"actionTypeId": {
"type": "keyword"
},
+ "isMissingSecrets": {
+ "type": "boolean"
+ },
"config": {
"enabled": false,
"type": "object"
From 6c635b3b98dc6a36f12e6f915205d4d4a945b487 Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Tue, 27 Apr 2021 15:34:28 -0600
Subject: [PATCH 21/70] Adds documentation about a painless script (#98509)
## Summary
Adds dev documentation about a painless script in our code and when we could remove it. See:
https://github.com/elastic/elasticsearch/issues/72276
https://github.com/elastic/kibana/pull/78912
---
.../security_solution/factory/hosts/details/helpers.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
index 00ed5c0c0dc01..a581370cb5720 100644
--- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
+++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts
@@ -55,6 +55,11 @@ const getTermsAggregationTypeFromField = (field: string): AggregationRequest =>
host_ip: {
terms: {
script: {
+ // We might be able to remove this when PR is fixed in Elasticsearch: https://github.com/elastic/elasticsearch/issues/72276
+ // Currently we cannot use "value_type" with an aggregation when we have a mapping conflict which is why this painless script exists
+ // See public ticket: https://github.com/elastic/kibana/pull/78912
+ // See private ticket: https://github.com/elastic/security-team/issues/333
+ // for more details on the use cases and causes of the conflicts and why this is here.
source: "doc['host.ip']",
lang: 'painless',
},
From 9dae1ef5b1ec29389de9ac46fee827e25be8fc40 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Wed, 28 Apr 2021 07:58:45 +0200
Subject: [PATCH 22/70] SOM: hide actions for hidden types (#98290)
* SOM: hide actions for hidden types
* fix FTR tests
* add and fix tests
* fix unit tests
* fix test types
* fix FTR test assertions
* add more FTR tests
* delete old file
---
.../saved_objects_management/common/index.ts | 2 +-
.../saved_objects_management/common/types.ts | 1 +
.../object_view/saved_object_view.tsx | 2 +-
.../saved_objects_table.test.tsx.snap | 2 +
.../components/delete_confirm_modal.tsx | 36 +-
.../saved_objects_table.test.tsx | 48 +-
.../objects_table/saved_objects_table.tsx | 7 +-
.../public/services/types/record.ts | 1 +
.../server/lib/find_relationships.test.ts | 1 +
.../server/lib/inject_meta_attributes.test.ts | 1 +
.../server/lib/inject_meta_attributes.ts | 1 +
.../server/services/management.mock.ts | 1 +
.../server/services/management.ts | 4 +
.../apis/saved_objects_management/find.ts | 5 +
.../saved_objects_management/relationships.ts | 16 +
.../hidden_types/data.json | 88 +++
.../hidden_types/mappings.json | 504 ++++++++++++++++++
.../management/saved_objects_page.ts | 8 +-
.../server/plugin.ts | 23 +-
.../saved_objects_hidden_type/index.ts | 1 -
.../interface/saved_objects_management.ts | 55 --
.../exports/_import_hidden_importable.ndjson | 0
.../_import_hidden_non_importable.ndjson | 0
.../saved_objects_management/hidden_types.ts | 127 +++++
.../saved_objects_management/index.ts | 1 +
.../copy_saved_objects_to_space_action.tsx | 2 +-
.../lib/summarize_copy_result.test.ts | 37 +-
.../share_saved_objects_to_space_action.tsx | 3 +-
28 files changed, 895 insertions(+), 82 deletions(-)
create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json
create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json
delete mode 100644 test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts
rename test/plugin_functional/test_suites/{saved_objects_hidden_type/interface => saved_objects_management}/exports/_import_hidden_importable.ndjson (100%)
rename test/plugin_functional/test_suites/{saved_objects_hidden_type/interface => saved_objects_management}/exports/_import_hidden_non_importable.ndjson (100%)
create mode 100644 test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts
diff --git a/src/plugins/saved_objects_management/common/index.ts b/src/plugins/saved_objects_management/common/index.ts
index 06db1eaa25de8..efabdace329c3 100644
--- a/src/plugins/saved_objects_management/common/index.ts
+++ b/src/plugins/saved_objects_management/common/index.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-export {
+export type {
SavedObjectWithMetadata,
SavedObjectMetadata,
SavedObjectRelation,
diff --git a/src/plugins/saved_objects_management/common/types.ts b/src/plugins/saved_objects_management/common/types.ts
index a6c25a6785e1a..7899cd0938ad3 100644
--- a/src/plugins/saved_objects_management/common/types.ts
+++ b/src/plugins/saved_objects_management/common/types.ts
@@ -19,6 +19,7 @@ export interface SavedObjectMetadata {
editUrl?: string;
inAppUrl?: { path: string; uiCapabilitiesPath: string };
namespaceType?: SavedObjectsNamespaceType;
+ hiddenType?: boolean;
}
/**
diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx
index cf4dcb7c6efda..2a7c56faa8507 100644
--- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx
@@ -89,7 +89,7 @@ export class SavedObjectEdition extends Component<
this.delete()}
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap
index fdd423e10a117..809cd7a96a0ed 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap
@@ -9,10 +9,12 @@ exports[`SavedObjectsTable delete should show a confirm modal 1`] = `
Array [
Object {
"id": "1",
+ "meta": Object {},
"type": "index-pattern",
},
Object {
"id": "3",
+ "meta": Object {},
"type": "dashboard",
},
]
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx
index f6f00c95d9bf1..d589d5a700801 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { FC } from 'react';
+import React, { FC, useMemo } from 'react';
import {
EuiInMemoryTable,
EuiLoadingElastic,
@@ -23,6 +23,7 @@ import {
EuiButtonEmpty,
EuiButton,
EuiSpacer,
+ EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -42,6 +43,13 @@ export const DeleteConfirmModal: FC = ({
onCancel,
selectedObjects,
}) => {
+ const undeletableObjects = useMemo(() => {
+ return selectedObjects.filter((obj) => obj.meta.hiddenType);
+ }, [selectedObjects]);
+ const deletableObjects = useMemo(() => {
+ return selectedObjects.filter((obj) => !obj.meta.hiddenType);
+ }, [selectedObjects]);
+
if (isDeleting) {
return (
@@ -49,7 +57,6 @@ export const DeleteConfirmModal: FC = ({
);
}
-
// can't use `EuiConfirmModal` here as the confirm modal body is wrapped
// inside a `` element, causing UI glitches with the table.
return (
@@ -63,6 +70,29 @@ export const DeleteConfirmModal: FC = ({
+ {undeletableObjects.length > 0 && (
+ <>
+
+ }
+ iconType="alert"
+ color="warning"
+ >
+
+
+
+
+
+ >
+ )}
= ({
{
const component = shallowRender();
const mockSelectedSavedObjects = [
- { id: '1', type: 'index-pattern' },
- { id: '3', type: 'dashboard' },
+ { id: '1', type: 'index-pattern', meta: {} },
+ { id: '3', type: 'dashboard', meta: {} },
] as SavedObjectWithMetadata[];
// Ensure all promises resolve
@@ -498,8 +498,8 @@ describe('SavedObjectsTable', () => {
it('should delete selected objects', async () => {
const mockSelectedSavedObjects = [
- { id: '1', type: 'index-pattern' },
- { id: '3', type: 'dashboard' },
+ { id: '1', type: 'index-pattern', meta: {} },
+ { id: '3', type: 'dashboard', meta: {} },
] as SavedObjectWithMetadata[];
const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({
@@ -529,7 +529,6 @@ describe('SavedObjectsTable', () => {
await component.instance().delete();
expect(defaultProps.indexPatterns.clearCache).toHaveBeenCalled();
- expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith(mockSelectedSavedObjects);
expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith(
mockSavedObjects[0].type,
mockSavedObjects[0].id,
@@ -542,5 +541,44 @@ describe('SavedObjectsTable', () => {
);
expect(component.state('selectedSavedObjects').length).toBe(0);
});
+
+ it('should not delete hidden selected objects', async () => {
+ const mockSelectedSavedObjects = [
+ { id: '1', type: 'index-pattern', meta: {} },
+ { id: '3', type: 'hidden-type', meta: { hiddenType: true } },
+ ] as SavedObjectWithMetadata[];
+
+ const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({
+ id: obj.id,
+ type: obj.type,
+ source: {},
+ }));
+
+ const mockSavedObjectsClient = {
+ ...defaultProps.savedObjectsClient,
+ bulkGet: jest.fn().mockImplementation(() => ({
+ savedObjects: mockSavedObjects,
+ })),
+ delete: jest.fn(),
+ };
+
+ const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
+
+ // Ensure all promises resolve
+ await new Promise((resolve) => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ // Set some as selected
+ component.instance().onSelectionChanged(mockSelectedSavedObjects);
+
+ await component.instance().delete();
+
+ expect(defaultProps.indexPatterns.clearCache).toHaveBeenCalled();
+ expect(mockSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
+ expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('index-pattern', '1', {
+ force: true,
+ });
+ });
});
});
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx
index 1d272e818ea1e..c207766918a70 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx
@@ -455,10 +455,9 @@ export class SavedObjectsTable extends Component
- savedObjectsClient.delete(object.type, object.id, { force: true })
- );
+ const deletes = selectedSavedObjects
+ .filter((object) => !object.meta.hiddenType)
+ .map((object) => savedObjectsClient.delete(object.type, object.id, { force: true }));
await Promise.all(deletes);
// Unset this
diff --git a/src/plugins/saved_objects_management/public/services/types/record.ts b/src/plugins/saved_objects_management/public/services/types/record.ts
index 17bdbc3a075f5..fc92c83cfc790 100644
--- a/src/plugins/saved_objects_management/public/services/types/record.ts
+++ b/src/plugins/saved_objects_management/public/services/types/record.ts
@@ -15,6 +15,7 @@ export interface SavedObjectsManagementRecord {
icon: string;
title: string;
namespaceType: SavedObjectsNamespaceType;
+ hiddenType: boolean;
};
references: SavedObjectReference[];
namespaces?: string[];
diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
index 686715aba7f17..0da14cbee4fd5 100644
--- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
+++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts
@@ -317,6 +317,7 @@ describe('findRelationships', () => {
title: 'title',
icon: 'icon',
editUrl: 'editUrl',
+ hiddenType: false,
inAppUrl: {
path: 'path',
uiCapabilitiesPath: 'uiCapabilitiesPath',
diff --git a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts
index bc775a03e276d..7b5f52d6bf968 100644
--- a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts
+++ b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts
@@ -49,6 +49,7 @@ describe('injectMetaAttributes', () => {
uiCapabilitiesPath: 'uiCapabilitiesPath',
},
namespaceType: 'single',
+ hiddenType: false,
},
});
});
diff --git a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts
index ee64010994109..d5b585371cbdf 100644
--- a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts
+++ b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts
@@ -25,6 +25,7 @@ export function injectMetaAttributes(
result.meta.editUrl = savedObjectsManagement.getEditUrl(savedObject);
result.meta.inAppUrl = savedObjectsManagement.getInAppUrl(savedObject);
result.meta.namespaceType = savedObjectsManagement.getNamespaceType(savedObject);
+ result.meta.hiddenType = savedObjectsManagement.isHidden(savedObject);
return result;
}
diff --git a/src/plugins/saved_objects_management/server/services/management.mock.ts b/src/plugins/saved_objects_management/server/services/management.mock.ts
index 6541c0d2847f5..2ab5bea4f8440 100644
--- a/src/plugins/saved_objects_management/server/services/management.mock.ts
+++ b/src/plugins/saved_objects_management/server/services/management.mock.ts
@@ -19,6 +19,7 @@ const createManagementMock = () => {
getEditUrl: jest.fn(),
getInAppUrl: jest.fn(),
getNamespaceType: jest.fn(),
+ isHidden: jest.fn().mockReturnValue(false),
};
return mocked;
};
diff --git a/src/plugins/saved_objects_management/server/services/management.ts b/src/plugins/saved_objects_management/server/services/management.ts
index 395ba639846a8..176c52c5a21bc 100644
--- a/src/plugins/saved_objects_management/server/services/management.ts
+++ b/src/plugins/saved_objects_management/server/services/management.ts
@@ -44,4 +44,8 @@ export class SavedObjectsManagement {
public getNamespaceType(savedObject: SavedObject) {
return this.registry.getType(savedObject.type)?.namespaceType;
}
+
+ public isHidden(savedObject: SavedObject) {
+ return this.registry.getType(savedObject.type)?.hidden ?? false;
+ }
}
diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts
index 8fb3884a5b37b..9bf3045bd0138 100644
--- a/test/api_integration/apis/saved_objects_management/find.ts
+++ b/test/api_integration/apis/saved_objects_management/find.ts
@@ -240,6 +240,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[0].meta).to.eql({
icon: 'discoverApp',
title: 'OneRecord',
+ hiddenType: false,
editUrl:
'/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
inAppUrl: {
@@ -259,6 +260,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[0].meta).to.eql({
icon: 'dashboardApp',
title: 'Dashboard',
+ hiddenType: false,
editUrl:
'/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357',
inAppUrl: {
@@ -278,6 +280,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[0].meta).to.eql({
icon: 'visualizeApp',
title: 'VisualizationFromSavedSearch',
+ hiddenType: false,
editUrl:
'/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357',
inAppUrl: {
@@ -289,6 +292,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[1].meta).to.eql({
icon: 'visualizeApp',
title: 'Visualization',
+ hiddenType: false,
editUrl:
'/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
inAppUrl: {
@@ -308,6 +312,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body.saved_objects[0].meta).to.eql({
icon: 'indexPatternApp',
title: 'saved_objects*',
+ hiddenType: false,
editUrl:
'/management/kibana/indexPatterns/patterns/8963ca30-3224-11e8-a572-ffca06da1357',
inAppUrl: {
diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts
index fee525067719f..17e562d221d72 100644
--- a/test/api_integration/apis/saved_objects_management/relationships.ts
+++ b/test/api_integration/apis/saved_objects_management/relationships.ts
@@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: schema.string(),
}),
namespaceType: schema.string(),
+ hiddenType: schema.boolean(),
}),
});
const invalidRelationSchema = schema.object({
@@ -89,6 +90,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'management.kibana.indexPatterns',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
{
@@ -105,6 +107,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
]);
@@ -132,6 +135,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'management.kibana.indexPatterns',
},
namespaceType: 'single',
+ hiddenType: false,
},
relationship: 'child',
},
@@ -148,6 +152,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
relationship: 'parent',
},
@@ -192,6 +197,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
{
@@ -208,6 +214,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
]);
@@ -232,6 +239,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
relationship: 'child',
},
@@ -248,6 +256,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
relationship: 'child',
},
@@ -292,6 +301,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'discover.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
{
@@ -308,6 +318,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'dashboard.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
]);
@@ -334,6 +345,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'discover.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
relationship: 'child',
},
@@ -378,6 +390,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'discover.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
{
@@ -394,6 +407,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
},
]);
@@ -420,6 +434,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'discover.show',
},
namespaceType: 'single',
+ hiddenType: false,
},
relationship: 'parent',
},
@@ -466,6 +481,7 @@ export default function ({ getService }: FtrProviderContext) {
uiCapabilitiesPath: 'visualize.show',
},
namespaceType: 'single',
+ hiddenType: false,
title: 'Visualization',
},
relationship: 'child',
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json
new file mode 100644
index 0000000000000..057373579c100
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json
@@ -0,0 +1,88 @@
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "test-actions-export-hidden:obj_1",
+ "source": {
+ "test-actions-export-hidden": {
+ "title": "hidden object 1"
+ },
+ "type": "test-actions-export-hidden",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "test-actions-export-hidden:obj_2",
+ "source": {
+ "test-actions-export-hidden": {
+ "title": "hidden object 2"
+ },
+ "type": "test-actions-export-hidden",
+ "migrationVersion": {},
+ "updated_at": "2018-12-21T00:43:07.096Z"
+ }
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed",
+ "source": {
+ "visualization": {
+ "title": "A Pie",
+ "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}",
+ "uiStateJSON": "{}",
+ "description": "",
+ "version": 1,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"
+ }
+ },
+ "type": "visualization",
+ "updated_at": "2019-01-22T19:32:31.206Z"
+ },
+ "references" : [
+ {
+ "name" : "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type" : "index-pattern",
+ "id" : "logstash-*"
+ }
+ ]
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "index": ".kibana",
+ "type": "doc",
+ "id": "dashboard:i-exist",
+ "source": {
+ "dashboard": {
+ "title": "A Dashboard",
+ "hits": 0,
+ "description": "",
+ "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]",
+ "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}",
+ "version": 1,
+ "timeRestore": false,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}"
+ }
+ },
+ "type": "dashboard",
+ "updated_at": "2019-01-22T19:32:47.232Z"
+ }
+ }
+}
diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json
new file mode 100644
index 0000000000000..a862731c13f7a
--- /dev/null
+++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json
@@ -0,0 +1,504 @@
+{
+ "type": "index",
+ "value": {
+ "index": ".kibana",
+ "settings": {
+ "index": {
+ "number_of_shards": "1",
+ "auto_expand_replicas": "0-1",
+ "number_of_replicas": "0"
+ }
+ },
+ "mappings": {
+ "dynamic": true,
+ "properties": {
+ "test-actions-export-hidden": {
+ "properties": {
+ "title": { "type": "text" }
+ }
+ },
+ "test-export-transform": {
+ "properties": {
+ "title": { "type": "text" },
+ "enabled": { "type": "boolean" }
+ }
+ },
+ "test-export-add": {
+ "properties": {
+ "title": { "type": "text" }
+ }
+ },
+ "test-export-add-dep": {
+ "properties": {
+ "title": { "type": "text" }
+ }
+ },
+ "test-export-transform-error": {
+ "properties": {
+ "title": { "type": "text" }
+ }
+ },
+ "test-export-invalid-transform": {
+ "properties": {
+ "title": { "type": "text" }
+ }
+ },
+ "apm-telemetry": {
+ "properties": {
+ "has_any_services": {
+ "type": "boolean"
+ },
+ "services_per_agent": {
+ "properties": {
+ "go": {
+ "type": "long",
+ "null_value": 0
+ },
+ "java": {
+ "type": "long",
+ "null_value": 0
+ },
+ "js-base": {
+ "type": "long",
+ "null_value": 0
+ },
+ "nodejs": {
+ "type": "long",
+ "null_value": 0
+ },
+ "python": {
+ "type": "long",
+ "null_value": 0
+ },
+ "ruby": {
+ "type": "long",
+ "null_value": 0
+ }
+ }
+ }
+ }
+ },
+ "canvas-workpad": {
+ "dynamic": "false",
+ "properties": {
+ "@created": {
+ "type": "date"
+ },
+ "@timestamp": {
+ "type": "date"
+ },
+ "id": {
+ "type": "text",
+ "index": false
+ },
+ "name": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ },
+ "config": {
+ "dynamic": "true",
+ "properties": {
+ "accessibility:disableAnimations": {
+ "type": "boolean"
+ },
+ "buildNum": {
+ "type": "keyword"
+ },
+ "dateFormat:tz": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "defaultIndex": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "telemetry:optIn": {
+ "type": "boolean"
+ }
+ }
+ },
+ "dashboard": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "optionsJSON": {
+ "type": "text"
+ },
+ "panelsJSON": {
+ "type": "text"
+ },
+ "refreshInterval": {
+ "properties": {
+ "display": {
+ "type": "keyword"
+ },
+ "pause": {
+ "type": "boolean"
+ },
+ "section": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ }
+ },
+ "timeFrom": {
+ "type": "keyword"
+ },
+ "timeRestore": {
+ "type": "boolean"
+ },
+ "timeTo": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "map": {
+ "properties": {
+ "bounds": {
+ "type": "geo_shape",
+ "tree": "quadtree"
+ },
+ "description": {
+ "type": "text"
+ },
+ "layerListJSON": {
+ "type": "text"
+ },
+ "mapStateJSON": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "graph-workspace": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "numLinks": {
+ "type": "integer"
+ },
+ "numVertices": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "wsState": {
+ "type": "text"
+ }
+ }
+ },
+ "index-pattern": {
+ "properties": {
+ "fieldFormatMap": {
+ "type": "text"
+ },
+ "fields": {
+ "type": "text"
+ },
+ "intervalName": {
+ "type": "keyword"
+ },
+ "notExpandable": {
+ "type": "boolean"
+ },
+ "sourceFilters": {
+ "type": "text"
+ },
+ "timeFieldName": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "typeMeta": {
+ "type": "keyword"
+ }
+ }
+ },
+ "kql-telemetry": {
+ "properties": {
+ "optInCount": {
+ "type": "long"
+ },
+ "optOutCount": {
+ "type": "long"
+ }
+ }
+ },
+ "migrationVersion": {
+ "dynamic": "true",
+ "properties": {
+ "index-pattern": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "space": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ }
+ }
+ },
+ "namespace": {
+ "type": "keyword"
+ },
+ "search": {
+ "properties": {
+ "columns": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "sort": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "server": {
+ "properties": {
+ "uuid": {
+ "type": "keyword"
+ }
+ }
+ },
+ "space": {
+ "properties": {
+ "_reserved": {
+ "type": "boolean"
+ },
+ "color": {
+ "type": "keyword"
+ },
+ "description": {
+ "type": "text"
+ },
+ "disabledFeatures": {
+ "type": "keyword"
+ },
+ "initials": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "spaceId": {
+ "type": "keyword"
+ },
+ "telemetry": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "timelion-sheet": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "hits": {
+ "type": "integer"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "timelion_chart_height": {
+ "type": "integer"
+ },
+ "timelion_columns": {
+ "type": "integer"
+ },
+ "timelion_interval": {
+ "type": "keyword"
+ },
+ "timelion_other_interval": {
+ "type": "keyword"
+ },
+ "timelion_rows": {
+ "type": "integer"
+ },
+ "timelion_sheet": {
+ "type": "text"
+ },
+ "title": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ }
+ }
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "updated_at": {
+ "type": "date"
+ },
+ "url": {
+ "properties": {
+ "accessCount": {
+ "type": "long"
+ },
+ "accessDate": {
+ "type": "date"
+ },
+ "createDate": {
+ "type": "date"
+ },
+ "url": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 2048
+ }
+ }
+ }
+ }
+ },
+ "visualization": {
+ "properties": {
+ "description": {
+ "type": "text"
+ },
+ "kibanaSavedObjectMeta": {
+ "properties": {
+ "searchSourceJSON": {
+ "type": "text"
+ }
+ }
+ },
+ "savedSearchId": {
+ "type": "keyword"
+ },
+ "title": {
+ "type": "text"
+ },
+ "uiStateJSON": {
+ "type": "text"
+ },
+ "version": {
+ "type": "integer"
+ },
+ "visState": {
+ "type": "text"
+ }
+ }
+ },
+ "references": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "name": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ }
+ },
+ "type": "nested"
+ }
+ }
+ }
+ }
+}
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
index c28d351aa77fb..fc4de6ed7f82f 100644
--- a/test/functional/page_objects/management/saved_objects_page.ts
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -294,10 +294,12 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
return await testSubjects.isEnabled('savedObjectsManagementDelete');
}
- async clickDelete() {
+ async clickDelete({ confirmDelete = true }: { confirmDelete?: boolean } = {}) {
await testSubjects.click('savedObjectsManagementDelete');
- await testSubjects.click('confirmModalConfirmButton');
- await this.waitTableIsLoaded();
+ if (confirmDelete) {
+ await testSubjects.click('confirmModalConfirmButton');
+ await this.waitTableIsLoaded();
+ }
}
async getImportWarnings() {
diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts
index daaf6426bdddc..408ac03dd946b 100644
--- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts
+++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts
@@ -90,8 +90,6 @@ export class SavedObjectExportTransformsPlugin implements Plugin {
},
});
- /////////////
- /////////////
// example of a SO type that will throw an object-transform-error
savedObjects.registerType({
name: 'test-export-transform-error',
@@ -134,8 +132,29 @@ export class SavedObjectExportTransformsPlugin implements Plugin {
},
},
});
+
+ // example of a SO type that is exportable while being hidden
+ savedObjects.registerType({
+ name: 'test-actions-export-hidden',
+ hidden: true,
+ namespaceType: 'single',
+ mappings: {
+ properties: {
+ title: { type: 'text' },
+ enabled: {
+ type: 'boolean',
+ },
+ },
+ },
+ management: {
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj) => obj.attributes.title,
+ },
+ });
}
public start() {}
+
public stop() {}
}
diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts
index 00ba74a988cf4..ba4835cdab089 100644
--- a/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts
+++ b/test/plugin_functional/test_suites/saved_objects_hidden_type/index.ts
@@ -15,6 +15,5 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
loadTestFile(require.resolve('./resolve_import_errors'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./delete'));
- loadTestFile(require.resolve('./interface/saved_objects_management'));
});
}
diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts b/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts
deleted file mode 100644
index dfd0b9dd07476..0000000000000
--- a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/saved_objects_management.ts
+++ /dev/null
@@ -1,55 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import path from 'path';
-import expect from '@kbn/expect';
-import { PluginFunctionalProviderContext } from '../../../services';
-
-export default function ({ getPageObjects, getService }: PluginFunctionalProviderContext) {
- const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
- const esArchiver = getService('esArchiver');
- const fixturePaths = {
- hiddenImportable: path.join(__dirname, 'exports', '_import_hidden_importable.ndjson'),
- hiddenNonImportable: path.join(__dirname, 'exports', '_import_hidden_non_importable.ndjson'),
- };
-
- describe('Saved objects management Interface', () => {
- before(() => esArchiver.emptyKibanaIndex());
- beforeEach(async () => {
- await PageObjects.settings.navigateTo();
- await PageObjects.settings.clickKibanaSavedObjects();
- });
- describe('importable/exportable hidden type', () => {
- it('imports objects successfully', async () => {
- await PageObjects.savedObjects.importFile(fixturePaths.hiddenImportable);
- await PageObjects.savedObjects.checkImportSucceeded();
- });
-
- it('shows test-hidden-importable-exportable in table', async () => {
- await PageObjects.savedObjects.searchForObject('type:(test-hidden-importable-exportable)');
- const results = await PageObjects.savedObjects.getTableSummary();
- expect(results.length).to.be(1);
-
- const { title } = results[0];
- expect(title).to.be(
- 'test-hidden-importable-exportable [id=ff3733a0-9fty-11e7-ahb3-3dcb94193fab]'
- );
- });
- });
-
- describe('non-importable/exportable hidden type', () => {
- it('fails to import object', async () => {
- await PageObjects.savedObjects.importFile(fixturePaths.hiddenNonImportable);
- await PageObjects.savedObjects.checkImportSucceeded();
-
- const errorsCount = await PageObjects.savedObjects.getImportErrorsCount();
- expect(errorsCount).to.be(1);
- });
- });
- });
-}
diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_importable.ndjson b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_hidden_importable.ndjson
similarity index 100%
rename from test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_importable.ndjson
rename to test/plugin_functional/test_suites/saved_objects_management/exports/_import_hidden_importable.ndjson
diff --git a/test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_non_importable.ndjson b/test/plugin_functional/test_suites/saved_objects_management/exports/_import_hidden_non_importable.ndjson
similarity index 100%
rename from test/plugin_functional/test_suites/saved_objects_hidden_type/interface/exports/_import_hidden_non_importable.ndjson
rename to test/plugin_functional/test_suites/saved_objects_management/exports/_import_hidden_non_importable.ndjson
diff --git a/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts
new file mode 100644
index 0000000000000..464b7c6e7ced7
--- /dev/null
+++ b/test/plugin_functional/test_suites/saved_objects_management/hidden_types.ts
@@ -0,0 +1,127 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import path from 'path';
+import expect from '@kbn/expect';
+import { PluginFunctionalProviderContext } from '../../services';
+
+const fixturePaths = {
+ hiddenImportable: path.join(__dirname, 'exports', '_import_hidden_importable.ndjson'),
+ hiddenNonImportable: path.join(__dirname, 'exports', '_import_hidden_non_importable.ndjson'),
+};
+
+export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
+ const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+ const testSubjects = getService('testSubjects');
+
+ describe('saved objects management with hidden types', () => {
+ before(async () => {
+ await esArchiver.load(
+ '../functional/fixtures/es_archiver/saved_objects_management/hidden_types'
+ );
+ });
+
+ after(async () => {
+ await esArchiver.unload(
+ '../functional/fixtures/es_archiver/saved_objects_management/hidden_types'
+ );
+ });
+
+ beforeEach(async () => {
+ await PageObjects.settings.navigateTo();
+ await PageObjects.settings.clickKibanaSavedObjects();
+ });
+
+ describe('API calls', () => {
+ it('should flag the object as hidden in its meta', async () => {
+ await supertest
+ .get('/api/kibana/management/saved_objects/_find?type=test-actions-export-hidden')
+ .set('kbn-xsrf', 'true')
+ .expect(200)
+ .then((resp) => {
+ expect(
+ resp.body.saved_objects.map((obj: any) => ({
+ id: obj.id,
+ type: obj.type,
+ hidden: obj.meta.hiddenType,
+ }))
+ ).to.eql([
+ {
+ id: 'obj_1',
+ type: 'test-actions-export-hidden',
+ hidden: true,
+ },
+ {
+ id: 'obj_2',
+ type: 'test-actions-export-hidden',
+ hidden: true,
+ },
+ ]);
+ });
+ });
+ });
+
+ describe('Delete modal', () => {
+ it('should display a warning then trying to delete hidden saved objects', async () => {
+ await PageObjects.savedObjects.clickCheckboxByTitle('A Pie');
+ await PageObjects.savedObjects.clickCheckboxByTitle('A Dashboard');
+ await PageObjects.savedObjects.clickCheckboxByTitle('hidden object 1');
+
+ await PageObjects.savedObjects.clickDelete({ confirmDelete: false });
+ expect(await testSubjects.exists('cannotDeleteObjectsConfirmWarning')).to.eql(true);
+ });
+
+ it('should not delete the hidden objects when performing the operation', async () => {
+ await PageObjects.savedObjects.clickCheckboxByTitle('A Pie');
+ await PageObjects.savedObjects.clickCheckboxByTitle('hidden object 1');
+
+ await PageObjects.savedObjects.clickDelete({ confirmDelete: true });
+
+ const objectNames = (await PageObjects.savedObjects.getTableSummary()).map(
+ (obj) => obj.title
+ );
+ expect(objectNames.includes('hidden object 1')).to.eql(true);
+ expect(objectNames.includes('A Pie')).to.eql(false);
+ });
+ });
+
+ describe('importing hidden types', () => {
+ describe('importable/exportable hidden type', () => {
+ it('imports objects successfully', async () => {
+ await PageObjects.savedObjects.importFile(fixturePaths.hiddenImportable);
+ await PageObjects.savedObjects.checkImportSucceeded();
+ });
+
+ it('shows test-hidden-importable-exportable in table', async () => {
+ await PageObjects.savedObjects.searchForObject(
+ 'type:(test-hidden-importable-exportable)'
+ );
+ const results = await PageObjects.savedObjects.getTableSummary();
+ expect(results.length).to.be(1);
+
+ const { title } = results[0];
+ expect(title).to.be(
+ 'test-hidden-importable-exportable [id=ff3733a0-9fty-11e7-ahb3-3dcb94193fab]'
+ );
+ });
+ });
+
+ describe('non-importable/exportable hidden type', () => {
+ it('fails to import object', async () => {
+ await PageObjects.savedObjects.importFile(fixturePaths.hiddenNonImportable);
+ await PageObjects.savedObjects.checkImportSucceeded();
+
+ const errorsCount = await PageObjects.savedObjects.getImportErrorsCount();
+ expect(errorsCount).to.be(1);
+ });
+ });
+ });
+ });
+}
diff --git a/test/plugin_functional/test_suites/saved_objects_management/index.ts b/test/plugin_functional/test_suites/saved_objects_management/index.ts
index 9f2d28b582f78..edaa819e5ea58 100644
--- a/test/plugin_functional/test_suites/saved_objects_management/index.ts
+++ b/test/plugin_functional/test_suites/saved_objects_management/index.ts
@@ -15,5 +15,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./export_transform'));
loadTestFile(require.resolve('./import_warnings'));
+ loadTestFile(require.resolve('./hidden_types'));
});
}
diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
index 57ea66f35ba0c..7818e648dd1cf 100644
--- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
+++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx
@@ -55,7 +55,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem
icon: 'copy',
type: 'icon',
available: (object: SavedObjectsManagementRecord) => {
- return object.meta.namespaceType !== 'agnostic';
+ return object.meta.namespaceType !== 'agnostic' && !object.meta.hiddenType;
},
onClick: (object: SavedObjectsManagementRecord) => {
this.start(object);
diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts
index 5a0ad95b6bead..6a3d82aaef59c 100644
--- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts
+++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts
@@ -33,7 +33,12 @@ const OBJECTS = {
MY_DASHBOARD: {
type: 'dashboard',
id: 'foo',
- meta: { title: 'my-dashboard-title', icon: 'dashboardApp', namespaceType: 'single' },
+ meta: {
+ title: 'my-dashboard-title',
+ icon: 'dashboardApp',
+ namespaceType: 'single',
+ hiddenType: false,
+ },
references: [
{ type: 'visualization', id: 'foo', name: 'Visualization foo' },
{ type: 'visualization', id: 'bar', name: 'Visualization bar' },
@@ -42,25 +47,45 @@ const OBJECTS = {
VISUALIZATION_FOO: {
type: 'visualization',
id: 'bar',
- meta: { title: 'visualization-foo-title', icon: 'visualizeApp', namespaceType: 'single' },
+ meta: {
+ title: 'visualization-foo-title',
+ icon: 'visualizeApp',
+ namespaceType: 'single',
+ hiddenType: false,
+ },
references: [{ type: 'index-pattern', id: 'foo', name: 'Index pattern foo' }],
} as SavedObjectsManagementRecord,
VISUALIZATION_BAR: {
type: 'visualization',
id: 'baz',
- meta: { title: 'visualization-bar-title', icon: 'visualizeApp', namespaceType: 'single' },
+ meta: {
+ title: 'visualization-bar-title',
+ icon: 'visualizeApp',
+ namespaceType: 'single',
+ hiddenType: false,
+ },
references: [{ type: 'index-pattern', id: 'bar', name: 'Index pattern bar' }],
} as SavedObjectsManagementRecord,
INDEX_PATTERN_FOO: {
type: 'index-pattern',
id: 'foo',
- meta: { title: 'index-pattern-foo-title', icon: 'indexPatternApp', namespaceType: 'single' },
+ meta: {
+ title: 'index-pattern-foo-title',
+ icon: 'indexPatternApp',
+ namespaceType: 'single',
+ hiddenType: false,
+ },
references: [],
} as SavedObjectsManagementRecord,
INDEX_PATTERN_BAR: {
type: 'index-pattern',
id: 'bar',
- meta: { title: 'index-pattern-bar-title', icon: 'indexPatternApp', namespaceType: 'single' },
+ meta: {
+ title: 'index-pattern-bar-title',
+ icon: 'indexPatternApp',
+ namespaceType: 'single',
+ hiddenType: false,
+ },
references: [],
} as SavedObjectsManagementRecord,
};
@@ -70,6 +95,7 @@ interface ObjectProperties {
id: string;
meta: { title?: string; icon?: string };
}
+
const createSuccessResult = ({ type, id, meta }: ObjectProperties) => {
return { type, id, meta };
};
@@ -190,6 +216,7 @@ describe('summarizeCopyResult', () => {
"obj": Object {
"id": "bar",
"meta": Object {
+ "hiddenType": false,
"icon": "visualizeApp",
"namespaceType": "single",
"title": "visualization-foo-title",
diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx
index 657ff8ac0bc70..9a0d171342a80 100644
--- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx
+++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx
@@ -40,7 +40,8 @@ export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManage
const hasCapability =
!this.actionContext ||
!!this.actionContext.capabilities.savedObjectsManagement.shareIntoSpace;
- return object.meta.namespaceType === 'multiple' && hasCapability;
+ const { namespaceType, hiddenType } = object.meta;
+ return namespaceType === 'multiple' && !hiddenType && hasCapability;
},
onClick: (object: SavedObjectsManagementRecord) => {
this.isDataChanged = false;
From c24c0d38f8ba40c61a29bb87f6c40da432869ae8 Mon Sep 17 00:00:00 2001
From: Yara Tercero
Date: Tue, 27 Apr 2021 23:06:53 -0700
Subject: [PATCH 23/70] [Security Solution][Tech Debt] - Fix flakey notes tab
cypress test (#98439)
Fix flakey cypress test related to notes_tab before/after hooks.
---
.../cypress/integration/timelines/notes_tab.spec.ts | 2 +-
.../plugins/security_solution/cypress/screens/timeline.ts | 2 +-
x-pack/plugins/security_solution/cypress/tasks/timeline.ts | 6 ++++--
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
index 6653290fc2ebb..ebf40677112e8 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts
@@ -41,8 +41,8 @@ describe('Timeline notes tab', () => {
.click({ force: true })
.then(() => {
waitForEventsPanelToBeLoaded();
- goToNotesTab();
addNotesToTimeline(timeline.notes);
+ goToNotesTab();
});
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
index 4c80f266e687c..0352320e25194 100644
--- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts
@@ -63,7 +63,7 @@ export const NOTE_CONTENT = (noteId: string) => `${NOTE_BY_NOTE_ID(noteId)} p`;
export const NOTES_TEXT_AREA = '[data-test-subj="add-a-note"] textarea';
-export const NOTES_TAB_BUTTON = '[data-test-subj="timelineTabs-notes"]';
+export const NOTES_TAB_BUTTON = 'button[data-test-subj="timelineTabs-notes"]';
export const NOTES_TEXT = '.euiMarkdownFormat';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
index 2078744393d94..89ced4b64cc2c 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
@@ -90,7 +90,9 @@ export const addNameAndDescriptionToTimeline = (timeline: Timeline) => {
};
export const goToNotesTab = () => {
- return cy.get(NOTES_TAB_BUTTON).click({ force: true });
+ cy.get(NOTES_TAB_BUTTON)
+ .pipe(($el) => $el.trigger('click'))
+ .should('be.visible');
};
export const getNotePreviewByNoteId = (noteId: string) => {
@@ -205,7 +207,7 @@ export const openTimelineTemplateFromSettings = (id: string) => {
};
export const openTimelineById = (timelineId: string) => {
- return cy.get(TIMELINE_TITLE_BY_ID(timelineId)).click({ force: true });
+ return cy.get(TIMELINE_TITLE_BY_ID(timelineId)).pipe(($el) => $el.trigger('click'));
};
export const pinFirstEvent = () => {
From bfb363f050200fa29c78f0999adc3d40d77927f2 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Wed, 28 Apr 2021 08:23:07 +0200
Subject: [PATCH 24/70] [ML] Transforms/Data Frame Analytics: Fix freezing
wizard for indices with massive amounts of fields. (#98259)
The transform wizard can become very slow when used with indices with e.g. 1000+ fields.
This PR fixes it by prefetching 500 random documents to create a list of populated/used fields and passes those on to the data grid component instead of all available fields from the list derived via Kibana index patterns.
For example, for an out of the box metricbeat index, this reduces the list of passed on fields from 3000+ to ~120 fields. Previously, the page would freeze on load for tens of seconds and would freeze again on every rerender. With the applied update, the page loads almost instantly again and remains responsive.
Note this fix of reducing available fields is only applied to the data grid preview component. All fields are still available to create the configuration in the UI for groups and aggregations. These UI components, e.g. the virtualized dropdowns, can handle large lists of fields.
---
.../hooks/use_index_data.ts | 83 +++++++++++++++----
.../public/app/hooks/use_index_data.ts | 61 +++++++++++++-
.../public/app/hooks/use_pivot_data.ts | 35 ++++++--
.../apps/transform/creation_index_pattern.ts | 28 ++++---
.../transform/creation_runtime_mappings.ts | 28 ++++++-
.../functional/services/transform/wizard.ts | 9 ++
6 files changed, 206 insertions(+), 38 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
index 2ae75083bff43..87dd6709e82f4 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
@@ -50,19 +50,21 @@ function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) {
});
}
-function getInitialColumns(indexPattern: IndexPattern) {
+function getInitialColumns(indexPattern: IndexPattern, fieldsFilter: string[]) {
const { fields } = newJobCapsServiceAnalytics;
- const columns = fields.map((field: any) => {
- const schema =
- getDataGridSchemaFromESFieldType(field.type) || getDataGridSchemaFromKibanaFieldType(field);
-
- return {
- id: field.name,
- schema,
- isExpandable: schema !== 'boolean',
- isRuntimeFieldColumn: false,
- };
- });
+ const columns = fields
+ .filter((field) => fieldsFilter.includes(field.name))
+ .map((field) => {
+ const schema =
+ getDataGridSchemaFromESFieldType(field.type) || getDataGridSchemaFromKibanaFieldType(field);
+
+ return {
+ id: field.name,
+ schema,
+ isExpandable: schema !== 'boolean',
+ isRuntimeFieldColumn: false,
+ };
+ });
// Add runtime fields defined in index pattern to columns
if (indexPattern) {
@@ -91,10 +93,57 @@ export const useIndexData = (
toastNotifications: CoreSetup['notifications']['toasts'],
runtimeMappings?: RuntimeMappings
): UseIndexDataReturnType => {
- const indexPatternFields = useMemo(() => getFieldsFromKibanaIndexPattern(indexPattern), [
- indexPattern,
- ]);
- const [columns, setColumns] = useState(getInitialColumns(indexPattern));
+ const [indexPatternFields, setIndexPatternFields] = useState();
+
+ // Fetch 500 random documents to determine populated fields.
+ // This is a workaround to avoid passing potentially thousands of unpopulated fields
+ // (for example, as part of filebeat/metricbeat/ECS based indices)
+ // to the data grid component which would significantly slow down the page.
+ const fetchDataGridSampleDocuments = async function () {
+ setErrorMessage('');
+ setStatus(INDEX_STATUS.LOADING);
+
+ const esSearchRequest = {
+ index: indexPattern.title,
+ body: {
+ fields: ['*'],
+ _source: false,
+ query: {
+ function_score: {
+ query: { match_all: {} },
+ random_score: {},
+ },
+ },
+ size: 500,
+ },
+ };
+
+ try {
+ const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest);
+ const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
+
+ // Get all field names for each returned doc and flatten it
+ // to a list of unique field names used across all docs.
+ const allKibanaIndexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern);
+ const populatedFields = [...new Set(docs.map(Object.keys).flat(1))].filter((d) =>
+ allKibanaIndexPatternFields.includes(d)
+ );
+
+ setStatus(INDEX_STATUS.LOADED);
+ setIndexPatternFields(populatedFields);
+ } catch (e) {
+ setErrorMessage(extractErrorMessage(e));
+ setStatus(INDEX_STATUS.ERROR);
+ }
+ };
+
+ useEffect(() => {
+ fetchDataGridSampleDocuments();
+ }, []);
+
+ const [columns, setColumns] = useState(
+ getInitialColumns(indexPattern, indexPatternFields ?? [])
+ );
const dataGrid = useDataGrid(columns);
const {
@@ -151,7 +200,7 @@ export const useIndexData = (
...(combinedRuntimeMappings ? getRuntimeFieldColumns(combinedRuntimeMappings) : []),
]);
} else {
- setColumns(getInitialColumns(indexPattern));
+ setColumns(getInitialColumns(indexPattern, indexPatternFields ?? []));
}
setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value);
setRowCountRelation(
diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
index f97693b8c038a..fe56537450c2b 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useEffect, useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import type { estypes } from '@elastic/elasticsearch';
import type { EuiDataGridColumn } from '@elastic/eui';
@@ -46,9 +46,66 @@ export const useIndexData = (
},
} = useAppDependencies();
- const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern);
+ const [indexPatternFields, setIndexPatternFields] = useState();
+
+ // Fetch 500 random documents to determine populated fields.
+ // This is a workaround to avoid passing potentially thousands of unpopulated fields
+ // (for example, as part of filebeat/metricbeat/ECS based indices)
+ // to the data grid component which would significantly slow down the page.
+ const fetchDataGridSampleDocuments = async function () {
+ setErrorMessage('');
+ setStatus(INDEX_STATUS.LOADING);
+
+ const esSearchRequest = {
+ index: indexPattern.title,
+ body: {
+ fields: ['*'],
+ _source: false,
+ query: {
+ function_score: {
+ query: { match_all: {} },
+ random_score: {},
+ },
+ },
+ size: 500,
+ },
+ };
+
+ const resp = await api.esSearch(esSearchRequest);
+
+ if (!isEsSearchResponse(resp)) {
+ setErrorMessage(getErrorMessage(resp));
+ setStatus(INDEX_STATUS.ERROR);
+ return;
+ }
+
+ const isCrossClusterSearch = indexPattern.title.includes(':');
+ const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined');
+
+ const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
+
+ // Get all field names for each returned doc and flatten it
+ // to a list of unique field names used across all docs.
+ const allKibanaIndexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern);
+ const populatedFields = [...new Set(docs.map(Object.keys).flat(1))].filter((d) =>
+ allKibanaIndexPatternFields.includes(d)
+ );
+
+ setCcsWarning(isCrossClusterSearch && isMissingFields);
+ setStatus(INDEX_STATUS.LOADED);
+ setIndexPatternFields(populatedFields);
+ };
+
+ useEffect(() => {
+ fetchDataGridSampleDocuments();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
const columns: EuiDataGridColumn[] = useMemo(() => {
+ if (typeof indexPatternFields === 'undefined') {
+ return [];
+ }
+
let result: Array<{ id: string; schema: string | undefined }> = [];
// Get the the runtime fields that are defined from API field and index patterns
diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts
index 24c28effd12bc..9a49ed9480359 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts
@@ -11,12 +11,12 @@ import { useEffect, useMemo, useState } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { getFlattenedObject } from '@kbn/std';
import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms';
import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards';
-import { getNestedProperty } from '../../../common/utils/object_utils';
import {
RenderCellValue,
@@ -159,13 +159,36 @@ export const usePivotData = (
return;
}
- setTableItems(resp.preview);
- setRowCount(resp.preview.length);
+ // To improve UI performance with a latest configuration for indices with a large number
+ // of fields, we reduce the number of available columns to those populated with values.
+
+ // 1. Flatten the returned object structure object documents to match mapping properties
+ const docs = resp.preview.map(getFlattenedObject);
+
+ // 2. Get all field names for each returned doc and flatten it
+ // to a list of unique field names used across all docs.
+ const populatedFields = [...new Set(docs.map(Object.keys).flat(1))];
+
+ // 3. Filter mapping properties by populated fields
+ const populatedProperties: PreviewMappingsProperties = Object.entries(
+ resp.generated_dest_index.mappings.properties
+ )
+ .filter(([key]) => populatedFields.includes(key))
+ .reduce(
+ (p, [key, value]) => ({
+ ...p,
+ [key]: value,
+ }),
+ {}
+ );
+
+ setTableItems(docs);
+ setRowCount(docs.length);
setRowCountRelation(ES_CLIENT_TOTAL_HITS_RELATION.EQ);
- setPreviewMappingsProperties(resp.generated_dest_index.mappings.properties);
+ setPreviewMappingsProperties(populatedProperties);
setStatus(INDEX_STATUS.LOADED);
- if (resp.preview.length === 0) {
+ if (docs.length === 0) {
setNoDataMessage(
i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', {
defaultMessage:
@@ -201,7 +224,7 @@ export const usePivotData = (
const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize;
const cellValue = pageData.hasOwnProperty(adjustedRowIndex)
- ? getNestedProperty(pageData[adjustedRowIndex], columnId, null)
+ ? pageData[adjustedRowIndex][columnId] ?? null
: null;
if (typeof cellValue === 'object' && cellValue !== null) {
diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts
index 61579ac68ae53..2c26a340a2a26 100644
--- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts
+++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts
@@ -166,11 +166,6 @@ export default function ({ getService }: FtrProviderContext) {
{ color: '#54B399', percentage: 90 },
],
},
- {
- chartAvailable: false,
- id: 'customer_birth_date',
- legend: '0 documents contain field.',
- },
{ chartAvailable: false, id: 'customer_first_name', legend: 'Chart not supported.' },
{ chartAvailable: false, id: 'customer_full_name', legend: 'Chart not supported.' },
{
@@ -210,6 +205,15 @@ export default function ({ getService }: FtrProviderContext) {
{ color: '#54B399', percentage: 75 },
],
},
+ {
+ chartAvailable: true,
+ id: 'day_of_week_i',
+ legend: '0 - 6',
+ colorStats: [
+ { color: '#000000', percentage: 20 },
+ { color: '#54B399', percentage: 75 },
+ ],
+ },
],
discoverQueryHits: '7,270',
},
@@ -296,7 +300,6 @@ export default function ({ getService }: FtrProviderContext) {
columns: 10,
rows: 5,
},
- histogramCharts: [],
discoverQueryHits: '10',
},
} as PivotTransformTestData,
@@ -336,7 +339,6 @@ export default function ({ getService }: FtrProviderContext) {
columns: 10,
rows: 5,
},
- histogramCharts: [],
transformPreview: {
column: 0,
values: [
@@ -404,10 +406,14 @@ export default function ({ getService }: FtrProviderContext) {
await transform.testExecution.logTestStep('enables the index preview histogram charts');
await transform.wizard.enableIndexPreviewHistogramCharts(true);
- await transform.testExecution.logTestStep('displays the index preview histogram charts');
- await transform.wizard.assertIndexPreviewHistogramCharts(
- testData.expected.histogramCharts
- );
+ if (Array.isArray(testData.expected.histogramCharts)) {
+ await transform.testExecution.logTestStep(
+ 'displays the index preview histogram charts'
+ );
+ await transform.wizard.assertIndexPreviewHistogramCharts(
+ testData.expected.histogramCharts
+ );
+ }
if (isPivotTransformTestData(testData)) {
await transform.testExecution.logTestStep('adds the group by entries');
diff --git a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts
index 1ecbdd41c219d..030748026af91 100644
--- a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts
+++ b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts
@@ -46,14 +46,37 @@ export default function ({ getService }: FtrProviderContext) {
await transform.api.cleanTransformIndices();
});
- // Only testing that histogram charts are available for runtime fields here
const histogramCharts: HistogramCharts = [
+ {
+ // Skipping colorStats assertion for this chart,
+ // results can be quite different on each run because of sampling.
+ chartAvailable: true,
+ id: '@timestamp',
+ },
+ { chartAvailable: false, id: '@version', legend: 'Chart not supported.' },
+ {
+ chartAvailable: true,
+ id: 'airline',
+ legend: '19 categories',
+ colorStats: [
+ { color: '#000000', percentage: 49 },
+ { color: '#54B399', percentage: 41 },
+ ],
+ },
+ {
+ chartAvailable: true,
+ id: 'responsetime',
+ colorStats: [
+ { color: '#54B399', percentage: 5 },
+ { color: '#000000', percentage: 95 },
+ ],
+ },
{
chartAvailable: true,
id: 'rt_airline_lower',
legend: '19 categories',
colorStats: [
- { color: '#000000', percentage: 48 },
+ { color: '#000000', percentage: 49 },
{ color: '#54B399', percentage: 41 },
],
},
@@ -65,6 +88,7 @@ export default function ({ getService }: FtrProviderContext) {
{ color: '#000000', percentage: 95 },
],
},
+ { chartAvailable: false, id: 'type', legend: 'Chart not supported.' },
];
const testDataList: Array = [
diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts
index f82af4f3a6d37..cef6c2724033e 100644
--- a/x-pack/test/functional/services/transform/wizard.ts
+++ b/x-pack/test/functional/services/transform/wizard.ts
@@ -237,6 +237,15 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi
// For each chart, get the content of each header cell and assert
// the legend text and column id and if the chart should be present or not.
await retry.tryForTime(5000, async () => {
+ const table = await testSubjects.find(`~transformIndexPreview`);
+ const $ = await table.parseDomContent();
+ const actualColumnLength = $('.euiDataGridHeaderCell__content').toArray().length;
+
+ expect(actualColumnLength).to.eql(
+ expectedHistogramCharts.length,
+ `Number of index preview column charts should be '${expectedHistogramCharts.length}' (got '${actualColumnLength}')`
+ );
+
for (const expected of expectedHistogramCharts.values()) {
const id = expected.id;
await testSubjects.existOrFail(`mlDataGridChart-${id}`);
From c408e3ae09e2959a05f3ee5d6a9e9c08d7d34365 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Wed, 28 Apr 2021 09:56:24 +0200
Subject: [PATCH 25/70] SO MigV2: fix logStateTransition (#98445)
* SO MigV2: fix logStateTransition
* extract func
* use info directly
* improve tests & impl
---
.../migrations_state_action_machine.test.ts | 36 +++++++++++++++++--
.../migrations_state_action_machine.ts | 17 ++++-----
.../saved_objects/migrationsv2/types.ts | 9 ++++-
3 files changed, 51 insertions(+), 11 deletions(-)
diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts
index 161d4a7219c8d..bffe590a39432 100644
--- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts
+++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts
@@ -10,10 +10,11 @@ import { migrationStateActionMachine } from './migrations_state_action_machine';
import { loggingSystemMock, elasticsearchServiceMock } from '../../mocks';
import * as Either from 'fp-ts/lib/Either';
import * as Option from 'fp-ts/lib/Option';
-import { AllControlStates, State } from './types';
-import { createInitialState } from './model';
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { elasticsearchClientMock } from '../../elasticsearch/client/mocks';
+import { LoggerAdapter } from '../../logging/logger_adapter';
+import { AllControlStates, State } from './types';
+import { createInitialState } from './model';
const esClient = elasticsearchServiceMock.createElasticsearchClient();
describe('migrationsStateActionMachine', () => {
@@ -146,6 +147,37 @@ describe('migrationsStateActionMachine', () => {
}
`);
});
+
+ // see https://github.com/elastic/kibana/issues/98406
+ it('correctly logs state transition when using a logger adapter', async () => {
+ const underlyingLogger = mockLogger.get();
+ const logger = new LoggerAdapter(underlyingLogger);
+
+ await expect(
+ migrationStateActionMachine({
+ initialState,
+ logger,
+ model: transitionModel(['LEGACY_REINDEX', 'LEGACY_DELETE', 'LEGACY_DELETE', 'DONE']),
+ next,
+ client: esClient,
+ })
+ ).resolves.toEqual(expect.anything());
+
+ const allLogs = loggingSystemMock.collect(mockLogger);
+ const stateTransitionLogs = allLogs.info
+ .map((call) => call[0])
+ .filter((log) => log.match('control state'));
+
+ expect(stateTransitionLogs).toMatchInlineSnapshot(`
+ Array [
+ "[.my-so-index] Log from LEGACY_REINDEX control state",
+ "[.my-so-index] Log from LEGACY_DELETE control state",
+ "[.my-so-index] Log from LEGACY_DELETE control state",
+ "[.my-so-index] Log from DONE control state",
+ ]
+ `);
+ });
+
it('resolves when reaching the DONE state', async () => {
await expect(
migrationStateActionMachine({
diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts
index dede52f9758e9..85cc86fe0a468 100644
--- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts
+++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts
@@ -49,14 +49,15 @@ const logStateTransition = (
tookMs: number
) => {
if (newState.logs.length > oldState.logs.length) {
- newState.logs.slice(oldState.logs.length).forEach((log) => {
- const getLogger = (level: keyof Logger) => {
- if (level === 'error') {
- return logger[level] as Logger['error'];
- }
- return logger[level] as Logger['info'];
- };
- getLogger(log.level)(logMessagePrefix + log.message);
+ newState.logs.slice(oldState.logs.length).forEach(({ message, level }) => {
+ switch (level) {
+ case 'error':
+ return logger.error(logMessagePrefix + message);
+ case 'info':
+ return logger.info(logMessagePrefix + message);
+ default:
+ throw new Error(`unexpected log level ${level}`);
+ }
});
}
diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts
index b84d483cf6203..50664bc9398fb 100644
--- a/src/core/server/saved_objects/migrationsv2/types.ts
+++ b/src/core/server/saved_objects/migrationsv2/types.ts
@@ -13,6 +13,13 @@ import { AliasAction } from './actions';
import { IndexMapping } from '../mappings';
import { SavedObjectsRawDoc } from '..';
+export type MigrationLogLevel = 'error' | 'info';
+
+export interface MigrationLog {
+ level: MigrationLogLevel;
+ message: string;
+}
+
export interface BaseState extends ControlState {
/** The first part of the index name such as `.kibana` or `.kibana_task_manager` */
readonly indexPrefix: string;
@@ -70,7 +77,7 @@ export interface BaseState extends ControlState {
* In this case, you should set a smaller batchSize value and restart the migration process again.
*/
readonly batchSize: number;
- readonly logs: Array<{ level: 'error' | 'info'; message: string }>;
+ readonly logs: MigrationLog[];
/**
* The current alias e.g. `.kibana` which always points to the latest
* version index
From 10a93366e0ab00e6f5bdc3057830cfa97462294c Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Wed, 28 Apr 2021 10:03:02 +0200
Subject: [PATCH 26/70] change config reload message (#98455)
* change config reload message
* change function name
---
src/core/server/bootstrap.ts | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts
index a2267635e86f2..18a5eceb1b2d3 100644
--- a/src/core/server/bootstrap.ts
+++ b/src/core/server/bootstrap.ts
@@ -46,22 +46,22 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
const root = new Root(rawConfigService, env, onRootShutdown);
- process.on('SIGHUP', () => reloadLoggingConfig());
+ process.on('SIGHUP', () => reloadConfiguration());
// This is only used by the LogRotator service
// in order to be able to reload the log configuration
// under the cluster mode
process.on('message', (msg) => {
- if (!msg || msg.reloadLoggingConfig !== true) {
+ if (!msg || msg.reloadConfiguration !== true) {
return;
}
- reloadLoggingConfig();
+ reloadConfiguration();
});
- function reloadLoggingConfig() {
+ function reloadConfiguration() {
const cliLogger = root.logger.get('cli');
- cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] });
+ cliLogger.info('Reloading Kibana configuration due to SIGHUP.', { tags: ['config'] });
try {
rawConfigService.reloadConfig();
@@ -69,7 +69,7 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
return shutdown(err);
}
- cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] });
+ cliLogger.info('Reloaded Kibana configuration due to SIGHUP.', { tags: ['config'] });
}
process.on('SIGINT', () => shutdown());
From 0cf12567840665ef555818ddf556766788440323 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Wed, 28 Apr 2021 10:12:29 +0200
Subject: [PATCH 27/70] always load test fixtures (#98464)
---
x-pack/test/functional/apps/lens/index.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts
index 38ba1f698ecce..bfb0aad7177f4 100644
--- a/x-pack/test/functional/apps/lens/index.ts
+++ b/x-pack/test/functional/apps/lens/index.ts
@@ -16,8 +16,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
before(async () => {
log.debug('Starting lens before method');
await browser.setWindowSize(1280, 800);
- await esArchiver.loadIfNeeded('logstash_functional');
- await esArchiver.loadIfNeeded('lens/basic');
+ await esArchiver.load('logstash_functional');
+ await esArchiver.load('lens/basic');
});
after(async () => {
From 3cc6989e5fffc8c458b09ed30fec95b5781d96fe Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 28 Apr 2021 13:59:19 +0300
Subject: [PATCH 28/70] [Visualize] Fixes V8 theme issues (#97090)
* [Visualize] Fixes K8 theme issues
* Fix ci
* Make it work for v7 too
* Fix
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../vis_default_editor/public/components/agg_add.tsx | 4 ++--
.../wizard/agg_based_selection/agg_based_selection.scss | 3 +++
.../wizard/agg_based_selection/agg_based_selection.tsx | 2 ++
.../public/application/components/visualize_listing.scss | 7 ++++++-
4 files changed, 13 insertions(+), 3 deletions(-)
create mode 100644 src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.scss
diff --git a/src/plugins/vis_default_editor/public/components/agg_add.tsx b/src/plugins/vis_default_editor/public/components/agg_add.tsx
index 37ef08ec640f0..bbe235082e13e 100644
--- a/src/plugins/vis_default_editor/public/components/agg_add.tsx
+++ b/src/plugins/vis_default_editor/public/components/agg_add.tsx
@@ -52,7 +52,7 @@ function DefaultEditorAggAdd({
const addButton = (
setIsPopoverOpen(!isPopoverOpen)}
@@ -88,7 +88,7 @@ function DefaultEditorAggAdd({
repositionOnScroll={true}
closePopover={() => setIsPopoverOpen(false)}
>
-
+
{(groupName !== AggGroupNames.Buckets || !stats.count) && (
}
+ className="aggBasedDialog__card"
/>
);
diff --git a/src/plugins/visualize/public/application/components/visualize_listing.scss b/src/plugins/visualize/public/application/components/visualize_listing.scss
index c3b0df67e317d..840ebf89c129f 100644
--- a/src/plugins/visualize/public/application/components/visualize_listing.scss
+++ b/src/plugins/visualize/public/application/components/visualize_listing.scss
@@ -21,7 +21,12 @@
}
.visListingCallout {
- max-width: 1000px;
+ @include kbnThemeStyle('v7') {
+ max-width: 1000px;
+ }
+ @include kbnThemeStyle('v8') {
+ max-width: 1200px;
+ }
width: 100%;
margin-left: auto;
From 66a1aff924295502b24cfb244944a0d97a2125aa Mon Sep 17 00:00:00 2001
From: Jorge Sanz
Date: Wed, 28 Apr 2021 14:02:35 +0200
Subject: [PATCH 29/70] [Maps] Updated documentation for Elastic Maps Server
7.13 (#98310)
Co-authored-by: Nick Peihl
Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
---
docs/maps/connect-to-ems.asciidoc | 17 +++++++++++------
.../images/elastic-maps-server-basemaps.png | Bin 0 -> 42518 bytes
.../elastic-maps-server-instructions.png | Bin 104953 -> 48671 bytes
3 files changed, 11 insertions(+), 6 deletions(-)
create mode 100644 docs/maps/images/elastic-maps-server-basemaps.png
diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc
index 8e4695bfc6662..88301123bae3f 100644
--- a/docs/maps/connect-to-ems.asciidoc
+++ b/docs/maps/connect-to-ems.asciidoc
@@ -40,11 +40,9 @@ To disable EMS, change your <> file.
[id=elastic-maps-server]
=== Host Elastic Maps Service locally
-beta::[]
-
If you cannot connect to Elastic Maps Service from the {kib} server or browser clients, and your cluster has the appropriate license level, you can opt to host the service on your own infrastructure.
-{hosted-ems} is a self-managed version of Elastic Maps Service offered as a Docker image that provides both the EMS basemaps and EMS boundaries. You must first download and run the image. After connecting it to your {es} cluster for license validation, you're guided to download and configure the basemaps database, which must be retrieved separately.
+{hosted-ems} is a self-managed version of Elastic Maps Service offered as a Docker image that provides both the EMS basemaps and EMS boundaries. The image is bundled with basemaps up to zoom level 8. After connecting it to your {es} cluster for license validation, you have the option to download and configure a more detailed basemaps database.
IMPORTANT: {hosted-ems} does not serve raster tiles, needed by Vega, coordinate, and region map visualizations.
@@ -69,7 +67,7 @@ docker run --rm --init --publish 8080:8080 \
{ems-docker-image}
----------------------------------
-Once {hosted-ems} is running, follow instructions from the webpage at `localhost:8080` to define a configuration file and download the basemaps database.
+Once {hosted-ems} is running, follow instructions from the webpage at `localhost:8080` to define a configuration file and optionally download a more detailed basemaps database.
[role="screenshot"]
image::images/elastic-maps-server-instructions.png[Set-up instructions]
@@ -92,6 +90,9 @@ endif::[]
| `port`
| Specifies the port used by the backend server. Default: *`8080`*. <>.
+| `basePath`
+ | Specify a path at which to mount the server if you are running behind a proxy. This setting cannot end in a slash (`/`). <>.
+
| `ui`
| Controls the display of the status page and the layer preview. *Default: `true`*
@@ -190,9 +191,13 @@ services:
[[elastic-maps-server-data]]
==== Data
-{hosted-ems} hosts vector layer boundaries and vector tile basemaps for the entire planet. Boundaries include world countries, global administrative regions, and specific country regions. A minimal basemap is provided with {hosted-ems}. This can be used for testing environments but won't be functional for normal operations. The full basemap (around 90GB file) needs to be mounted on the Docker container for {hosted-ems} to run normally.
+{hosted-ems} hosts vector layer boundaries and vector tile basemaps for the entire planet. Boundaries include world countries, global administrative regions, and specific country regions. Basemaps up to zoom level 8 are bundled in the Docker image. These basemaps are sufficient for maps and dashboards at the country level. To present maps with higher detail, follow the instructions of the front page to download and configure the appropriate basemaps database. The most detailed basemaps at zoom level 14 are good for street level maps, but require ~90GB of disk space.
+
+
+[role="screenshot"]
+image::images/elastic-maps-server-basemaps.png[Basemaps download options]
-TIP: The available basemaps and boundaries can be explored from the `/maps` endpoint in a web page that is your self-managed equivalent to https://maps.elastic.co
+TIP: The available basemaps and boundaries can be explored from the `/maps` endpoint in a web page that is your self-managed equivalent to https://maps.elastic.co.
[float]
diff --git a/docs/maps/images/elastic-maps-server-basemaps.png b/docs/maps/images/elastic-maps-server-basemaps.png
new file mode 100644
index 0000000000000000000000000000000000000000..3f51153d2394b9a93e42ea6cda419b0ef8342ae4
GIT binary patch
literal 42518
zcmYg$18`@*_x7i@Z5vzLwvDavsokw@+qP}n*4EsbTX&0pzrUGx=AF68+~g)F&&`>W
zJSRC3N(zz)u(+@Q002Q+N=yX+0LlMO=Fp(uHN}6{%K!k1YELyS7ZoFSVh1OCbD)hG
zv5SX;8L`uSr*unJzQKU@=wq+xR|*sM6=%>e=4n@Z;a-d0pPt*fT=eulG|A
zgznHWs1m`S`X?U`b67vu91aivl?n*nJ$e+LK-Hl^>aX7qc?wGHJjXrjd*_y4jUjcc
z{Pis#68w2`-_M(TLiEL@QsfCA=$G|uKP-~0aUe4Ak3g^f`Q{B