- Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{"mappingRoles":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping-roles.html","createRoleMapping":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html","createRoleMappingTemplates":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html#_role_templates","roleMappingFieldRules":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/role-mapping-resources.html#mapping-roles-rule-field"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}}
+ Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}}
- Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{"mappingRoles":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping-roles.html","createRoleMapping":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html","createRoleMappingTemplates":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html#_role_templates","roleMappingFieldRules":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/role-mapping-resources.html#mapping-roles-rule-field"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}}
+ Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}}
`);
diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx
index ce4ded5a9acbc..28b452c10c237 100644
--- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx
+++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx
@@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n';
import { StartServicesAccessor } from 'src/core/public';
import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public';
import { PluginStartDependencies } from '../../plugin';
-import { DocumentationLinksService } from './documentation_links';
import { tryDecodeURIComponent } from '../url_utils';
+import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
interface CreateParams {
getStartServices: StartServicesAccessor;
@@ -39,7 +39,7 @@ export const roleMappingsManagementApp = Object.freeze({
];
const [
- [{ docLinks, http, notifications, i18n: i18nStart }],
+ [core],
{ RoleMappingsGridPage },
{ EditRoleMappingPage },
{ RoleMappingsAPIClient },
@@ -52,16 +52,15 @@ export const roleMappingsManagementApp = Object.freeze({
import('../roles'),
]);
- const roleMappingsAPIClient = new RoleMappingsAPIClient(http);
- const dockLinksService = new DocumentationLinksService(docLinks);
+ const roleMappingsAPIClient = new RoleMappingsAPIClient(core.http);
const RoleMappingsGridPageWithBreadcrumbs = () => {
setBreadcrumbs(roleMappingsBreadcrumbs);
return (
@@ -90,27 +89,29 @@ export const roleMappingsManagementApp = Object.freeze({
);
};
render(
-
-
-
-
-
-
-
-
-
-
-
- ,
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
element
);
diff --git a/x-pack/plugins/security/public/management/roles/documentation_links.ts b/x-pack/plugins/security/public/management/roles/documentation_links.ts
deleted file mode 100644
index aa19fbecb9c7b..0000000000000
--- a/x-pack/plugins/security/public/management/roles/documentation_links.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DocLinksStart } from 'src/core/public';
-
-export class DocumentationLinksService {
- private readonly esClusterPrivileges: string;
- private readonly esRunAsPrivilege: string;
- private readonly esIndicesPrivileges: string;
-
- constructor(docLinks: DocLinksStart) {
- this.esClusterPrivileges = `${docLinks.links.security.clusterPrivileges}`;
- this.esRunAsPrivilege = `${docLinks.links.security.runAsPrivilege}`;
- this.esIndicesPrivileges = `${docLinks.links.security.indicesPrivileges}`;
- }
-
- public getESClusterPrivilegesDocUrl() {
- return `${this.esClusterPrivileges}`;
- }
-
- public getESRunAsPrivilegesDocUrl() {
- return `${this.esRunAsPrivilege}`;
- }
-
- public getESIndicesPrivilegesDocUrl() {
- return `${this.esIndicesPrivileges}`;
- }
-}
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
index b86fa1f175e96..e431b49bf2f84 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
@@ -11,7 +11,6 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest';
import { Capabilities } from 'src/core/public';
import { KibanaFeature } from '../../../../../features/public';
import { Role } from '../../../../common/model';
-import { DocumentationLinksService } from '../documentation_links';
import { EditRolePage } from './edit_role_page';
import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section';
@@ -184,7 +183,7 @@ function getProps({
userAPIClient,
getFeatures: () => Promise.resolve(buildFeatures()),
notifications,
- docLinks: new DocumentationLinksService(docLinks),
+ docLinks,
fatalErrors,
uiCapabilities: buildUICapabilities(canManageSpaces),
history: scopedHistoryMock.create(),
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx
index d24191c54bd94..c750ec373b9f7 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx
@@ -38,7 +38,7 @@ import {
IHttpFetchError,
NotificationsStart,
} from 'src/core/public';
-import { ScopedHistory } from 'kibana/public';
+import type { DocLinksStart, ScopedHistory } from 'kibana/public';
import { FeaturesPluginStart } from '../../../../../features/public';
import { KibanaFeature } from '../../../../../features/common';
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
@@ -61,7 +61,6 @@ import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges';
import { ReservedRoleBadge } from './reserved_role_badge';
import { SecurityLicense } from '../../../../common/licensing';
import { UserAPIClient } from '../../users';
-import { DocumentationLinksService } from '../documentation_links';
import { IndicesAPIClient } from '../indices_api_client';
import { RolesAPIClient } from '../roles_api_client';
import { PrivilegesAPIClient } from '../privileges_api_client';
@@ -77,7 +76,7 @@ interface Props {
rolesAPIClient: PublicMethodsOf;
privilegesAPIClient: PublicMethodsOf;
getFeatures: FeaturesPluginStart['getFeatures'];
- docLinks: DocumentationLinksService;
+ docLinks: DocLinksStart;
http: HttpStart;
license: SecurityLicense;
uiCapabilities: Capabilities;
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx
index 316822f7db024..4a29c5a5b134c 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx
@@ -6,7 +6,6 @@
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
-import { DocumentationLinksService } from '../../../documentation_links';
import { RoleValidator } from '../../validate_role';
import { ClusterPrivileges } from './cluster_privileges';
import { ElasticsearchPrivileges } from './elasticsearch_privileges';
@@ -45,7 +44,7 @@ function getProps() {
index: ['all', 'read', 'write', 'index'],
},
indicesAPIClient: indicesAPIClientMock.create(),
- docLinks: new DocumentationLinksService(docLinks),
+ docLinks,
license,
};
}
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx
index 8fc09ce167400..ca7a086639051 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx
@@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { PublicMethodsOf } from '@kbn/utility-types';
import React, { Component, Fragment } from 'react';
+import type { DocLinksStart } from 'src/core/public';
import { Role, BuiltinESPrivileges } from '../../../../../../common/model';
import { SecurityLicense } from '../../../../../../common/licensing';
import { IndicesAPIClient } from '../../../indices_api_client';
@@ -26,13 +27,12 @@ import { RoleValidator } from '../../validate_role';
import { CollapsiblePanel } from '../../collapsible_panel';
import { ClusterPrivileges } from './cluster_privileges';
import { IndexPrivileges } from './index_privileges';
-import { DocumentationLinksService } from '../../../documentation_links';
interface Props {
role: Role;
editable: boolean;
indicesAPIClient: PublicMethodsOf;
- docLinks: DocumentationLinksService;
+ docLinks: DocLinksStart;
license: SecurityLicense;
onChange: (role: Role) => void;
runAsUsers: string[];
@@ -90,7 +90,7 @@ export class ElasticsearchPrivileges extends Component {
id="xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription"
defaultMessage="Manage the actions this role can perform against your cluster. "
/>
- {this.learnMore(docLinks.getESClusterPrivilegesDocUrl())}
+ {this.learnMore(docLinks.links.security.clusterPrivileges)}
}
>
@@ -120,7 +120,7 @@ export class ElasticsearchPrivileges extends Component {
id="xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription"
defaultMessage="Allow requests to be submitted on the behalf of other users. "
/>
- {this.learnMore(docLinks.getESRunAsPrivilegesDocUrl())}
+ {this.learnMore(docLinks.links.security.runAsPrivilege)}
}
>
@@ -164,7 +164,7 @@ export class ElasticsearchPrivileges extends Component {
id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription"
defaultMessage="Control access to the data in your cluster. "
/>
- {this.learnMore(docLinks.getESIndicesPrivilegesDocUrl())}
+ {this.learnMore(docLinks.links.security.indicesPrivileges)}
diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx
index 8003b21f5d906..12c1951fc60f0 100644
--- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx
@@ -11,7 +11,9 @@ jest.mock('./roles_grid', () => ({
}));
jest.mock('./edit_role', () => ({
- EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`,
+ // `docLinks` object is too big to include into test snapshot, so we just check its existence.
+ EditRolePage: (props: any) =>
+ `Role Edit Page: ${JSON.stringify({ ...props, docLinks: props.docLinks ? {} : undefined })}`,
}));
import { rolesManagementApp } from './roles_management_app';
@@ -87,7 +89,7 @@ describe('rolesManagementApp', () => {
expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }, { text: 'Create' }]);
expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esClusterPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-cluster","esRunAsPrivilege":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#_run_as_privilege","esIndicesPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
+ Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
- Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esClusterPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-cluster","esRunAsPrivilege":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#_run_as_privilege","esIndicesPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
+ Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
- Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esClusterPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-cluster","esRunAsPrivilege":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#_run_as_privilege","esIndicesPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
+ Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
`);
diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx
index d5b3b4998a09d..26c679516b46e 100644
--- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx
+++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx
@@ -12,7 +12,6 @@ import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public';
import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public';
import { SecurityLicense } from '../../../common/licensing';
import { PluginStartDependencies } from '../../plugin';
-import { DocumentationLinksService } from './documentation_links';
import { tryDecodeURIComponent } from '../url_utils';
interface CreateParams {
@@ -97,7 +96,7 @@ export const rolesManagementApp = Object.freeze({
notifications={notifications}
fatalErrors={fatalErrors}
license={license}
- docLinks={new DocumentationLinksService(docLinks)}
+ docLinks={docLinks}
uiCapabilities={application.capabilities}
indexPatterns={data.indexPatterns}
history={history}
diff --git a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx
index 310caeac91dc1..844444c0e64d5 100644
--- a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx
+++ b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx
@@ -7,7 +7,7 @@ import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
import { render, unmountComponentAtNode } from 'react-dom';
-import { MountPoint } from 'kibana/public';
+import type { DocLinksStart, MountPoint } from 'kibana/public';
import {
EuiCheckbox,
EuiText,
@@ -16,7 +16,6 @@ import {
EuiFlexItem,
EuiButton,
} from '@elastic/eui';
-import { DocumentationLinksService } from '../documentation_links';
export const insecureClusterAlertTitle = i18n.translate(
'xpack.security.checkup.insecureClusterTitle',
@@ -24,12 +23,15 @@ export const insecureClusterAlertTitle = i18n.translate(
);
export const insecureClusterAlertText = (
- getDocLinksService: () => DocumentationLinksService,
+ getDocLinks: () => DocLinksStart,
onDismiss: (persist: boolean) => void
) =>
((e) => {
const AlertText = () => {
const [persist, setPersist] = useState(false);
+ const enableSecurityDocLink = `${
+ getDocLinks().links.security.elasticsearchEnableSecurity
+ }?blade=kibanasecuritymessage`;
return (
@@ -56,7 +58,7 @@ export const insecureClusterAlertText = (
size="s"
color="primary"
fill
- href={getDocLinksService().getEnableSecurityDocUrl()}
+ href={enableSecurityDocLink}
target="_blank"
data-test-subj="learnMoreButton"
>
diff --git a/x-pack/plugins/security/public/security_checkup/documentation_links.ts b/x-pack/plugins/security/public/security_checkup/documentation_links.ts
deleted file mode 100644
index 4a2a2bc968cc6..0000000000000
--- a/x-pack/plugins/security/public/security_checkup/documentation_links.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DocLinksStart } from 'src/core/public';
-
-export class DocumentationLinksService {
- private readonly esEnableSecurity: string;
-
- constructor(docLinks: DocLinksStart) {
- this.esEnableSecurity = `${docLinks.links.security.elasticsearchEnableSecurity}`;
- }
-
- public getEnableSecurityDocUrl() {
- return `${this.esEnableSecurity}?blade=kibanasecuritymessage`;
- }
-}
diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx
index a0ea194170dff..bda9f210b02ec 100644
--- a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx
+++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx
@@ -4,14 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { DocLinksStart } from 'kibana/public';
+import type { DocLinksStart } from 'kibana/public';
import {
SecurityOssPluginSetup,
SecurityOssPluginStart,
} from '../../../../../src/plugins/security_oss/public';
import { insecureClusterAlertTitle, insecureClusterAlertText } from './components';
-import { DocumentationLinksService } from './documentation_links';
interface SetupDeps {
securityOssSetup: SecurityOssPluginSetup;
@@ -25,13 +24,13 @@ interface StartDeps {
export class SecurityCheckupService {
private securityOssStart?: SecurityOssPluginStart;
- private docLinksService?: DocumentationLinksService;
+ private docLinks?: DocLinksStart;
public setup({ securityOssSetup }: SetupDeps) {
securityOssSetup.insecureCluster.setAlertTitle(insecureClusterAlertTitle);
securityOssSetup.insecureCluster.setAlertText(
insecureClusterAlertText(
- () => this.docLinksService!,
+ () => this.docLinks!,
(persist: boolean) => this.onDismiss(persist)
)
);
@@ -39,7 +38,7 @@ export class SecurityCheckupService {
public start({ securityOssStart, docLinks }: StartDeps) {
this.securityOssStart = securityOssStart;
- this.docLinksService = new DocumentationLinksService(docLinks);
+ this.docLinks = docLinks;
}
private onDismiss(persist: boolean) {
From 3dcb98ec670e4b1b44febdf632523c9e5dbea955 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Fri, 15 Jan 2021 13:32:16 +0100
Subject: [PATCH 03/38] [ILM] Absolute to relative time conversion (#87822)
* cleaning up unused types and legacy logic
* added new relative age logic with unit tests
* export the calculate relative timing function that returns millisecond values
* added exports to index.ts file
* copy update and test update
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../common/types/policies.ts | 19 -
.../data_tiers/determine_allocation_type.ts | 34 +-
...absolute_timing_to_relative_timing.test.ts | 507 ++++++++++++++++++
.../lib/absolute_timing_to_relative_timing.ts | 185 +++++++
.../sections/edit_policy/lib/index.ts | 12 +
5 files changed, 705 insertions(+), 52 deletions(-)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts
index 58468f06e3b2d..1f4b06e80c49f 100644
--- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts
+++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts
@@ -154,25 +154,6 @@ export interface PhaseWithMinAge {
selectedMinimumAgeUnits: string;
}
-/**
- * Different types of allocation markers we use in deserialized policies.
- *
- * default - use data tier based data allocation based on node roles -- this is ES best practice mode.
- * custom - use node_attrs to allocate data to specific nodes
- * none - do not move data anywhere when entering a phase
- */
-export type DataTierAllocationType = 'default' | 'custom' | 'none';
-
-export interface PhaseWithAllocationAction {
- selectedNodeAttrs: string;
- selectedReplicaCount: string;
- /**
- * A string value indicating allocation type. If unspecified we assume the user
- * wants to use default allocation.
- */
- dataTierAllocationType: DataTierAllocationType;
-}
-
export interface PhaseWithIndexPriority {
phaseIndexPriority: string;
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts
index 20ac439e9964f..6dde03ec4593f 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts
@@ -4,39 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { DataTierAllocationType, AllocateAction, MigrateAction } from '../../../../common/types';
-
-/**
- * Determine what deserialized state the policy config represents.
- *
- * See {@DataTierAllocationType} for more information.
- */
-export const determineDataTierAllocationTypeLegacy = (
- actions: {
- allocate?: AllocateAction;
- migrate?: MigrateAction;
- } = {}
-): DataTierAllocationType => {
- const { allocate, migrate } = actions;
-
- if (migrate?.enabled === false) {
- return 'none';
- }
-
- if (!allocate) {
- return 'default';
- }
-
- if (
- (allocate.require && Object.keys(allocate.require).length) ||
- (allocate.include && Object.keys(allocate.include).length) ||
- (allocate.exclude && Object.keys(allocate.exclude).length)
- ) {
- return 'custom';
- }
-
- return 'default';
-};
+import { AllocateAction, MigrateAction } from '../../../../common/types';
export const determineDataTierAllocationType = (
actions: {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
new file mode 100644
index 0000000000000..28910871fa33b
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts
@@ -0,0 +1,507 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { deserializer } from '../form';
+
+import {
+ absoluteTimingToRelativeTiming,
+ calculateRelativeTimingMs,
+} from './absolute_timing_to_relative_timing';
+
+describe('Conversion of absolute policy timing to relative timing', () => {
+ describe('calculateRelativeTimingMs', () => {
+ describe('policy that never deletes data (keep forever)', () => {
+ test('always hot', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({ total: Infinity, phases: { hot: Infinity, warm: undefined, cold: undefined } });
+ });
+
+ test('hot, then always warm', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({ total: Infinity, phases: { hot: 0, warm: Infinity, cold: undefined } });
+ });
+
+ test('hot, then warm, then always cold', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '1M',
+ actions: {},
+ },
+ cold: {
+ min_age: '34d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: Infinity,
+ phases: {
+ hot: 2592000000,
+ warm: 345600000,
+ cold: Infinity,
+ },
+ });
+ });
+
+ test('hot, then always cold', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ cold: {
+ min_age: '34d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: Infinity,
+ phases: { hot: 2937600000, warm: undefined, cold: Infinity },
+ });
+ });
+ });
+
+ describe('policy that deletes data', () => {
+ test('hot, then delete', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ delete: {
+ min_age: '1M',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: 2592000000,
+ phases: {
+ hot: 2592000000,
+ warm: undefined,
+ cold: undefined,
+ },
+ });
+ });
+
+ test('hot, then warm, then delete', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '24d',
+ actions: {},
+ },
+ delete: {
+ min_age: '1M',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: 2592000000,
+ phases: {
+ hot: 2073600000,
+ warm: 518400000,
+ cold: undefined,
+ },
+ });
+ });
+
+ test('hot, then warm, then cold, then delete', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '24d',
+ actions: {},
+ },
+ cold: {
+ min_age: '2M',
+ actions: {},
+ },
+ delete: {
+ min_age: '2d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: 5270400000,
+ phases: {
+ hot: 2073600000,
+ warm: 3196800000,
+ cold: 0,
+ },
+ });
+ });
+
+ test('hot, then cold, then delete', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ cold: {
+ min_age: '2M',
+ actions: {},
+ },
+ delete: {
+ min_age: '2d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: 5270400000,
+ phases: {
+ hot: 5270400000,
+ warm: undefined,
+ cold: 0,
+ },
+ });
+ });
+
+ test('hot, then long warm, then short cold, then delete', () => {
+ expect(
+ calculateRelativeTimingMs(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '2M',
+ actions: {},
+ },
+ cold: {
+ min_age: '1d',
+ actions: {},
+ },
+ delete: {
+ min_age: '2d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: 5270400000,
+ phases: {
+ hot: 5270400000,
+ warm: 0,
+ cold: 0,
+ },
+ });
+ });
+ });
+ });
+
+ describe('absoluteTimingToRelativeTiming', () => {
+ describe('policy that never deletes data (keep forever)', () => {
+ test('always hot', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({ total: 'Forever', hot: 'Forever', warm: undefined, cold: undefined });
+ });
+
+ test('hot, then always warm', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({ total: 'Forever', hot: 'Less than a day', warm: 'Forever', cold: undefined });
+ });
+
+ test('hot, then warm, then always cold', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '1M',
+ actions: {},
+ },
+ cold: {
+ min_age: '34d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: 'Forever',
+ hot: '30 days',
+ warm: '4 days',
+ cold: 'Forever',
+ });
+ });
+
+ test('hot, then always cold', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ cold: {
+ min_age: '34d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({ total: 'Forever', hot: '34 days', warm: undefined, cold: 'Forever' });
+ });
+ });
+
+ describe('policy that deletes data', () => {
+ test('hot, then delete', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ delete: {
+ min_age: '1M',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: '30 days',
+ hot: '30 days',
+ warm: undefined,
+ cold: undefined,
+ });
+ });
+
+ test('hot, then warm, then delete', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '24d',
+ actions: {},
+ },
+ delete: {
+ min_age: '1M',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: '30 days',
+ hot: '24 days',
+ warm: '6 days',
+ cold: undefined,
+ });
+ });
+
+ test('hot, then warm, then cold, then delete', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '24d',
+ actions: {},
+ },
+ cold: {
+ min_age: '2M',
+ actions: {},
+ },
+ delete: {
+ min_age: '2d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: '61 days',
+ hot: '24 days',
+ warm: '37 days',
+ cold: 'Less than a day',
+ });
+ });
+
+ test('hot, then cold, then delete', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ cold: {
+ min_age: '2M',
+ actions: {},
+ },
+ delete: {
+ min_age: '2d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: '61 days',
+ hot: '61 days',
+ warm: undefined,
+ cold: 'Less than a day',
+ });
+ });
+
+ test('hot, then long warm, then short cold, then delete', () => {
+ expect(
+ absoluteTimingToRelativeTiming(
+ deserializer({
+ name: 'test',
+ phases: {
+ hot: {
+ min_age: '0ms',
+ actions: {},
+ },
+ warm: {
+ min_age: '2M',
+ actions: {},
+ },
+ cold: {
+ min_age: '1d',
+ actions: {},
+ },
+ delete: {
+ min_age: '2d',
+ actions: {},
+ },
+ },
+ })
+ )
+ ).toEqual({
+ total: '61 days',
+ hot: '61 days',
+ warm: 'Less than a day',
+ cold: 'Less than a day',
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
new file mode 100644
index 0000000000000..c77b171a56bed
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts
@@ -0,0 +1,185 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * READ ME:
+ *
+ * ILM policies express data age thresholds as minimum age from an absolute point of reference.
+ * The absolute point of reference could be when data was created, but it could also be when
+ * rollover has occurred. This is useful for configuring a policy, but when trying to understand
+ * how long data will be in a specific phase, when thinking of data tiers, it is not as useful.
+ *
+ * This code converts the absolute timings to _relative_ timings of the form: 30 days in hot phase,
+ * 40 days in warm phase then forever in cold phase.
+ */
+
+import moment from 'moment';
+import { flow } from 'fp-ts/lib/function';
+import { i18n } from '@kbn/i18n';
+
+import { splitSizeAndUnits } from '../../../lib/policies';
+
+import { FormInternal } from '../types';
+
+type MinAgePhase = 'warm' | 'cold' | 'delete';
+
+type Phase = 'hot' | MinAgePhase;
+
+const i18nTexts = {
+ forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.Forever', {
+ defaultMessage: 'Forever',
+ }),
+ lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', {
+ defaultMessage: 'Less than a day',
+ }),
+ day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', {
+ defaultMessage: 'day',
+ }),
+ days: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.days', {
+ defaultMessage: 'days',
+ }),
+};
+
+interface AbsoluteTimings {
+ hot: {
+ min_age: undefined;
+ };
+ warm?: {
+ min_age: string;
+ };
+ cold?: {
+ min_age: string;
+ };
+ delete?: {
+ min_age: string;
+ };
+}
+
+export interface PhaseAgeInMilliseconds {
+ total: number;
+ phases: {
+ hot: number;
+ warm?: number;
+ cold?: number;
+ };
+}
+
+const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete'];
+
+const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({
+ min_age: formData.phases[phase]?.min_age
+ ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit
+ : '0ms',
+});
+
+const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => {
+ const { _meta } = formData;
+ if (!_meta) {
+ return { hot: { min_age: undefined } };
+ }
+ return {
+ hot: { min_age: undefined },
+ warm: _meta.warm.enabled ? getMinAge('warm', formData) : undefined,
+ cold: _meta.cold.enabled ? getMinAge('cold', formData) : undefined,
+ delete: _meta.delete.enabled ? getMinAge('delete', formData) : undefined,
+ };
+};
+
+/**
+ * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math
+ * for all date math values. ILM policies also support "micros" and "nanos".
+ */
+const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => {
+ let milliseconds: number;
+ const { units, size } = splitSizeAndUnits(phase.min_age);
+ if (units === 'micros') {
+ milliseconds = parseInt(size, 10) / 1e3;
+ } else if (units === 'nanos') {
+ milliseconds = parseInt(size, 10) / 1e6;
+ } else {
+ milliseconds = moment.duration(size, units as any).asMilliseconds();
+ }
+ return milliseconds;
+};
+
+/**
+ * Given a set of phase minimum age absolute timings, like hot phase 0ms and warm phase 3d, work out
+ * the number of milliseconds data will reside in phase.
+ */
+const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds => {
+ return phaseOrder.reduce(
+ (acc, phaseName, idx) => {
+ // Delete does not have an age associated with it
+ if (phaseName === 'delete') {
+ return acc;
+ }
+ const phase = inputs[phaseName];
+ if (!phase) {
+ return acc;
+ }
+ const nextPhase = phaseOrder
+ .slice(idx + 1)
+ .find((nextPhaseName) => Boolean(inputs[nextPhaseName])); // find the next existing phase
+
+ let nextPhaseMinAge = Infinity;
+
+ // If we have a next phase, calculate the timing between this phase and the next
+ if (nextPhase && inputs[nextPhase]?.min_age) {
+ nextPhaseMinAge = getPhaseMinAgeInMilliseconds(inputs[nextPhase] as { min_age: string });
+ }
+
+ return {
+ // data will be the age of the phase with the highest min age requirement
+ total: Math.max(acc.total, nextPhaseMinAge),
+ phases: {
+ ...acc.phases,
+ [phaseName]: Math.max(nextPhaseMinAge - acc.total, 0), // get the max age for the current phase, take 0 if negative number
+ },
+ };
+ },
+ {
+ total: 0,
+ phases: {
+ hot: 0,
+ warm: inputs.warm ? 0 : undefined,
+ cold: inputs.cold ? 0 : undefined,
+ },
+ }
+ );
+};
+
+const millisecondsToDays = (milliseconds?: number): string | undefined => {
+ if (milliseconds == null) {
+ return;
+ }
+ if (!isFinite(milliseconds)) {
+ return i18nTexts.forever;
+ }
+ const days = milliseconds / 8.64e7;
+ return days < 1
+ ? i18nTexts.lessThanADay
+ : `${Math.floor(days)} ${days === 1 ? i18nTexts.day : i18nTexts.days}`;
+};
+
+export const normalizeTimingsToHumanReadable = ({
+ total,
+ phases,
+}: PhaseAgeInMilliseconds): { total?: string; hot?: string; warm?: string; cold?: string } => {
+ return {
+ total: millisecondsToDays(total),
+ hot: millisecondsToDays(phases.hot),
+ warm: millisecondsToDays(phases.warm),
+ cold: millisecondsToDays(phases.cold),
+ };
+};
+
+export const calculateRelativeTimingMs = flow(formDataToAbsoluteTimings, calculateMilliseconds);
+
+export const absoluteTimingToRelativeTiming = flow(
+ formDataToAbsoluteTimings,
+ calculateMilliseconds,
+ normalizeTimingsToHumanReadable
+);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
new file mode 100644
index 0000000000000..9593fcc810a6f
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export {
+ absoluteTimingToRelativeTiming,
+ calculateRelativeTimingMs,
+ normalizeTimingsToHumanReadable,
+ PhaseAgeInMilliseconds,
+} from './absolute_timing_to_relative_timing';
From 701bd0998d70ccacdd5118cd573a2c6cfb29d664 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Fri, 15 Jan 2021 13:39:31 +0100
Subject: [PATCH 04/38] Bump backport to 5.6.4 (#88428)
---
package.json | 2 +-
yarn.lock | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index df8a41f29bf79..07984e5a237b0 100644
--- a/package.json
+++ b/package.json
@@ -589,7 +589,7 @@
"babel-plugin-require-context-hook": "^1.0.0",
"babel-plugin-styled-components": "^1.10.7",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
- "backport": "^5.6.1",
+ "backport": "^5.6.4",
"base64-js": "^1.3.1",
"base64url": "^3.0.1",
"broadcast-channel": "^3.0.3",
diff --git a/yarn.lock b/yarn.lock
index 5a39eacf3e68b..8595ed43bf6d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8194,7 +8194,7 @@ bach@^1.0.0:
async-settle "^1.0.0"
now-and-later "^2.0.0"
-backport@^5.6.1:
+backport@^5.6.4:
version "5.6.4"
resolved "https://registry.yarnpkg.com/backport/-/backport-5.6.4.tgz#8cf4bc750b26d27161306858ee9069218ad7cdfd"
integrity sha512-ZhuZcGxOBHBXFBCwweVf02b+KhWe0tdgg71jPSl583YYxlru+JBRH+TFM8S0J6/6YUuTWO81M9funjehJ18jqg==
From 686ece9aeade49dd4a4a6e82b63781a92a72fea5 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Fri, 15 Jan 2021 12:57:41 +0000
Subject: [PATCH 05/38] [ML] Improving existing job check in anomaly detection
wizard (#87674)
* [ML] Improving existing job check in anomaly detection wizard
* fixing job id validation
* allow group ids to be reused
* updating module exists endpoint
* fixing issuse with job without group list
* fixing test and translation ids
* fixing validator when model plot is disabled
* changes based on review
* adding group id check to edit job flyout
* small refactor and fixing edit job issue
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/ml/common/types/job_service.ts | 24 +++
.../edit_job_flyout/edit_job_flyout.js | 27 ++--
.../common/job_validator/job_validator.ts | 34 +++--
.../jobs/new_job/common/job_validator/util.ts | 32 ----
.../common/job_validator/validators.ts | 138 ++++++++++++++----
.../components/groups/groups_input.tsx | 20 ++-
.../components/job_id/job_id_input.tsx | 27 ++--
.../job_details_step/job_details.tsx | 2 +
.../jobs/new_job/pages/new_job/page.tsx | 2 +-
.../services/ml_api_service/jobs.ts | 17 ++-
.../models/data_recognizer/data_recognizer.ts | 4 +-
.../ml/server/models/job_service/groups.ts | 2 +-
.../ml/server/models/job_service/jobs.ts | 35 +++--
.../models/results_service/results_service.ts | 2 +-
.../apis/ml/jobs/jobs_exist_spaces.ts | 15 +-
15 files changed, 252 insertions(+), 129 deletions(-)
create mode 100644 x-pack/plugins/ml/common/types/job_service.ts
diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts
new file mode 100644
index 0000000000000..3121c3a82a387
--- /dev/null
+++ b/x-pack/plugins/ml/common/types/job_service.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Job, JobStats } from './anomaly_detection_jobs';
+
+export interface MlJobsResponse {
+ jobs: Job[];
+ count: number;
+}
+
+export interface MlJobsStatsResponse {
+ jobs: JobStats[];
+ count: number;
+}
+
+export interface JobsExistResponse {
+ [jobId: string]: {
+ exists: boolean;
+ isGroup: boolean;
+ };
+}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js
index bd781d32a6b06..97f9515810a60 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js
@@ -29,6 +29,7 @@ import { saveJob } from './edit_utils';
import { loadFullJob } from '../utils';
import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job';
import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service';
+import { ml } from '../../../../services/ml_api_service';
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
import { collapseLiteralStrings } from '../../../../../../shared_imports';
import { DATAFEED_STATE } from '../../../../../../common/constants/states';
@@ -195,16 +196,24 @@ export class EditJobFlyoutUI extends Component {
}
if (jobDetails.jobGroups !== undefined) {
- if (jobDetails.jobGroups.some((j) => this.props.allJobIds.includes(j))) {
- jobGroupsValidationError = i18n.translate(
- 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage',
- {
- defaultMessage:
- 'A job with this ID already exists. Groups and jobs cannot use the same ID.',
+ jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message;
+ if (jobGroupsValidationError === '') {
+ ml.jobs.jobsExist(jobDetails.jobGroups, true).then((resp) => {
+ const groups = Object.values(resp);
+ const valid = groups.some((g) => g.exists === true && g.isGroup === false) === false;
+ if (valid === false) {
+ this.setState({
+ jobGroupsValidationError: i18n.translate(
+ 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage',
+ {
+ defaultMessage:
+ 'A job with this ID already exists. Groups and jobs cannot use the same ID.',
+ }
+ ),
+ isValidJobDetails: false,
+ });
}
- );
- } else {
- jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message;
+ });
}
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts
index 1c012033e97c8..cf08de196a7d8 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts
@@ -14,9 +14,15 @@ import {
} from '../../../../../../common/util/job_utils';
import { getNewJobLimits } from '../../../../services/ml_server_info';
import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator';
-import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util';
-import { ExistingJobsAndGroups } from '../../../../services/job_service';
-import { cardinalityValidator, CardinalityValidatorResult } from './validators';
+import { populateValidationMessages } from './util';
+import {
+ cardinalityValidator,
+ CardinalityValidatorResult,
+ jobIdValidator,
+ groupIdsValidator,
+ JobExistsResult,
+ GroupsExistResult,
+} from './validators';
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../common/constants/categorization_job';
import { JOB_TYPE } from '../../../../../../common/constants/new_job';
@@ -25,7 +31,9 @@ import { JOB_TYPE } from '../../../../../../common/constants/new_job';
// after every keystroke
export const VALIDATION_DELAY_MS = 500;
-type AsyncValidatorsResult = Partial;
+type AsyncValidatorsResult = Partial<
+ CardinalityValidatorResult & JobExistsResult & GroupsExistResult
+>;
/**
* Union of possible validation results.
@@ -69,7 +77,6 @@ export class JobValidator {
private _validateTimeout: ReturnType | null = null;
private _asyncValidators$: Array> = [];
private _asyncValidatorsResult$: Observable;
- private _existingJobsAndGroups: ExistingJobsAndGroups;
private _basicValidations: BasicValidations = {
jobId: { valid: true },
groupIds: { valid: true },
@@ -97,7 +104,7 @@ export class JobValidator {
*/
public validationResult$: Observable;
- constructor(jobCreator: JobCreatorType, existingJobsAndGroups: ExistingJobsAndGroups) {
+ constructor(jobCreator: JobCreatorType) {
this._jobCreator = jobCreator;
this._lastJobConfig = this._jobCreator.formattedJobJson;
this._lastDatafeedConfig = this._jobCreator.formattedDatafeedJson;
@@ -105,9 +112,12 @@ export class JobValidator {
basic: false,
advanced: false,
};
- this._existingJobsAndGroups = existingJobsAndGroups;
- this._asyncValidators$ = [cardinalityValidator(this._jobCreatorSubject$)];
+ this._asyncValidators$ = [
+ cardinalityValidator(this._jobCreatorSubject$),
+ jobIdValidator(this._jobCreatorSubject$),
+ groupIdsValidator(this._jobCreatorSubject$),
+ ];
this._asyncValidatorsResult$ = combineLatest(this._asyncValidators$).pipe(
map((res) => {
@@ -208,14 +218,6 @@ export class JobValidator {
datafeedConfig
);
- // run addition job and group id validation
- const idResults = checkForExistingJobAndGroupIds(
- this._jobCreator.jobId,
- this._jobCreator.groups,
- this._existingJobsAndGroups
- );
- populateValidationMessages(idResults, this._basicValidations, jobConfig, datafeedConfig);
-
this._validationSummary.basic = this._isOverallBasicValid();
// Update validation results subject
this._basicValidationResult$.next(this._basicValidations);
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
index 04be935ed4399..9bdb4c7708b40 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts
@@ -13,8 +13,6 @@ import {
} from '../../../../../../common/constants/validation';
import { getNewJobLimits } from '../../../../services/ml_server_info';
import { ValidationResults } from '../../../../../../common/util/job_utils';
-import { ExistingJobsAndGroups } from '../../../../services/job_service';
-import { JobValidationMessage } from '../../../../../../common/constants/messages';
export function populateValidationMessages(
validationResults: ValidationResults,
@@ -204,36 +202,6 @@ export function populateValidationMessages(
}
}
-export function checkForExistingJobAndGroupIds(
- jobId: string,
- groupIds: string[],
- existingJobsAndGroups: ExistingJobsAndGroups
-): ValidationResults {
- const messages: JobValidationMessage[] = [];
-
- // check that job id does not already exist as a job or group or a newly created group
- if (
- existingJobsAndGroups.jobIds.includes(jobId) ||
- existingJobsAndGroups.groupIds.includes(jobId) ||
- groupIds.includes(jobId)
- ) {
- messages.push({ id: 'job_id_already_exists' });
- }
-
- // check that groups that have been newly added in this job do not already exist as job ids
- const newGroups = groupIds.filter((g) => !existingJobsAndGroups.groupIds.includes(g));
- if (existingJobsAndGroups.jobIds.some((g) => newGroups.includes(g))) {
- messages.push({ id: 'job_group_id_already_exists' });
- }
-
- return {
- messages,
- valid: messages.length === 0,
- contains: (id: string) => messages.some((m) => id === m.id),
- find: (id: string) => messages.find((m) => id === m.id),
- };
-}
-
function invalidTimeIntervalMessage(value: string | undefined) {
return i18n.translate(
'xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage',
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts
index eabf5588579c5..d17c5507722f4 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
-import { Observable, Subject } from 'rxjs';
+import { i18n } from '@kbn/i18n';
+import { distinctUntilChanged, filter, map, pluck, switchMap, startWith } from 'rxjs/operators';
+import { combineLatest, Observable, Subject } from 'rxjs';
import {
CardinalityModelPlotHigh,
CardinalityValidationResult,
@@ -13,6 +14,7 @@ import {
} from '../../../../services/ml_api_service';
import { JobCreator } from '../job_creator';
import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs';
+import { BasicValidations } from './job_validator';
export enum VALIDATOR_SEVERITY {
ERROR,
@@ -26,8 +28,30 @@ export interface CardinalityValidatorError {
};
}
+const jobExistsErrorMessage = i18n.translate(
+ 'xpack.ml.newJob.wizard.validateJob.asyncJobNameAlreadyExists',
+ {
+ defaultMessage:
+ 'Job ID already exists. A job ID cannot be the same as an existing job or group.',
+ }
+);
+const groupExistsErrorMessage = i18n.translate(
+ 'xpack.ml.newJob.wizard.validateJob.asyncGroupNameAlreadyExists',
+ {
+ defaultMessage:
+ 'Group ID already exists. A group ID cannot be the same as an existing group or job.',
+ }
+);
+
export type CardinalityValidatorResult = CardinalityValidatorError | null;
+export type JobExistsResult = {
+ jobIdExists: BasicValidations['jobId'];
+} | null;
+export type GroupsExistResult = {
+ groupIdsExist: BasicValidations['groupIds'];
+} | null;
+
export function isCardinalityModelPlotHigh(
cardinalityValidationResult: CardinalityValidationResult
): cardinalityValidationResult is CardinalityModelPlotHigh {
@@ -39,39 +63,95 @@ export function isCardinalityModelPlotHigh(
export function cardinalityValidator(
jobCreator$: Subject
): Observable {
+ return combineLatest([
+ jobCreator$.pipe(pluck('modelPlot')),
+ jobCreator$.pipe(
+ filter((jobCreator) => {
+ return jobCreator?.modelPlot;
+ }),
+ map((jobCreator) => {
+ return {
+ jobCreator,
+ analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config, null, 2),
+ };
+ }),
+ distinctUntilChanged((prev, curr) => {
+ return prev.analysisConfigString === curr.analysisConfigString;
+ }),
+ switchMap(({ jobCreator }) => {
+ // Perform a cardinality check only with enabled model plot.
+ return ml
+ .validateCardinality$({
+ ...jobCreator.jobConfig,
+ datafeed_config: jobCreator.datafeedConfig,
+ } as CombinedJob)
+ .pipe(
+ map((validationResults) => {
+ for (const validationResult of validationResults) {
+ if (isCardinalityModelPlotHigh(validationResult)) {
+ return {
+ highCardinality: {
+ value: validationResult.modelPlotCardinality,
+ severity: VALIDATOR_SEVERITY.WARNING,
+ },
+ };
+ }
+ }
+ return null;
+ })
+ );
+ }),
+ startWith(null)
+ ),
+ ]).pipe(
+ map(([isModelPlotEnabled, cardinalityValidationResult]) => {
+ return isModelPlotEnabled ? cardinalityValidationResult : null;
+ })
+ );
+}
+
+export function jobIdValidator(jobCreator$: Subject): Observable {
return jobCreator$.pipe(
- // Perform a cardinality check only with enabled model plot.
- filter((jobCreator) => {
- return jobCreator?.modelPlot;
- }),
map((jobCreator) => {
+ return jobCreator.jobId;
+ }),
+ // No need to perform an API call if the analysis configuration hasn't been changed
+ distinctUntilChanged((prevJobId, currJobId) => prevJobId === currJobId),
+ switchMap((jobId) => ml.jobs.jobsExist$([jobId], true)),
+ map((jobExistsResults) => {
+ const jobs = Object.values(jobExistsResults);
+ const valid = jobs?.[0].exists === false;
return {
- jobCreator,
- analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config),
+ jobIdExists: {
+ valid,
+ ...(valid ? {} : { message: jobExistsErrorMessage }),
+ },
};
- }),
+ })
+ );
+}
+
+export function groupIdsValidator(jobCreator$: Subject): Observable {
+ return jobCreator$.pipe(
+ map((jobCreator) => jobCreator.groups),
// No need to perform an API call if the analysis configuration hasn't been changed
- distinctUntilChanged((prev, curr) => {
- return prev.analysisConfigString === curr.analysisConfigString;
- }),
- switchMap(({ jobCreator }) => {
- return ml.validateCardinality$({
- ...jobCreator.jobConfig,
- datafeed_config: jobCreator.datafeedConfig,
- } as CombinedJob);
+ distinctUntilChanged(
+ (prevGroups, currGroups) => JSON.stringify(prevGroups) === JSON.stringify(currGroups)
+ ),
+ switchMap((groups) => {
+ return ml.jobs.jobsExist$(groups, true);
}),
- map((validationResults) => {
- for (const validationResult of validationResults) {
- if (isCardinalityModelPlotHigh(validationResult)) {
- return {
- highCardinality: {
- value: validationResult.modelPlotCardinality,
- severity: VALIDATOR_SEVERITY.WARNING,
- },
- };
- }
- }
- return null;
+ map((jobExistsResults) => {
+ const groups = Object.values(jobExistsResults);
+ // only match jobs that exist but aren't groups.
+ // as we should allow existing groups to be reused.
+ const valid = groups.some((g) => g.exists === true && g.isGroup === false) === false;
+ return {
+ groupIdsExist: {
+ valid,
+ ...(valid ? {} : { message: groupExistsErrorMessage }),
+ },
+ };
})
);
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx
index a693127e07f48..3fd191d653826 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC, useState, useContext, useEffect } from 'react';
+import React, { FC, useState, useContext, useEffect, useMemo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { JobCreatorContext } from '../../../job_creator_context';
@@ -17,7 +17,19 @@ export const GroupsInput: FC = () => {
);
const { existingJobsAndGroups } = useContext(JobCreatorContext);
const [selectedGroups, setSelectedGroups] = useState(jobCreator.groups);
- const [validation, setValidation] = useState(jobValidator.groupIds);
+
+ const validation = useMemo(() => {
+ const valid =
+ jobValidator.groupIds.valid === true &&
+ jobValidator.latestValidationResult.groupIdsExist?.valid === true;
+ const message =
+ jobValidator.groupIds.message ?? jobValidator.latestValidationResult.groupIdsExist?.message;
+
+ return {
+ valid,
+ message,
+ };
+ }, [jobValidatorUpdated]);
useEffect(() => {
jobCreator.groups = selectedGroups;
@@ -61,10 +73,6 @@ export const GroupsInput: FC = () => {
setSelectedGroups([...selectedOptions, newGroup].map((g) => g.label));
}
- useEffect(() => {
- setValidation(jobValidator.groupIds);
- }, [jobValidatorUpdated]);
-
return (
{
JobCreatorContext
);
const [jobId, setJobId] = useState(jobCreator.jobId);
- const [validation, setValidation] = useState(jobValidator.jobId);
+
+ const validation = useMemo(() => {
+ const isEmptyId = jobId === '';
+ const valid =
+ isEmptyId === true ||
+ (jobValidator.jobId.valid === true &&
+ jobValidator.latestValidationResult.jobIdExists?.valid === true);
+
+ const message =
+ jobValidator.jobId.message ?? jobValidator.latestValidationResult.jobIdExists?.message;
+
+ return {
+ valid,
+ message,
+ };
+ }, [jobValidatorUpdated]);
useEffect(() => {
jobCreator.jobId = jobId;
jobCreatorUpdate();
}, [jobId]);
- useEffect(() => {
- const isEmptyId = jobId === '';
- setValidation({
- valid: isEmptyId === true || jobValidator.jobId.valid,
- message: isEmptyId === false ? jobValidator.jobId.message : '',
- });
- }, [jobValidatorUpdated]);
-
return (
= ({
jobValidator.jobId.valid &&
jobValidator.modelMemoryLimit.valid &&
jobValidator.groupIds.valid &&
+ jobValidator.latestValidationResult.jobIdExists?.valid === true &&
+ jobValidator.latestValidationResult.groupIdsExist?.valid === true &&
jobValidator.validating === false;
setNextActive(active);
}, [jobValidatorUpdated]);
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
index 8e223b69b00e8..8f7f93763fdd6 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx
@@ -182,7 +182,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => {
const chartLoader = new ChartLoader(mlContext.currentIndexPattern, mlContext.combinedQuery);
- const jobValidator = new JobValidator(jobCreator, existingJobsAndGroups);
+ const jobValidator = new JobValidator(jobCreator);
const resultsLoader = new ResultsLoader(jobCreator, chartInterval, chartLoader);
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
index d356fc0ef339b..10e035103dbec 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { Observable } from 'rxjs';
import { HttpService } from '../http_service';
import { basePath } from './index';
@@ -23,6 +24,7 @@ import {
} from '../../../../common/types/categories';
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
import { Category } from '../../../../common/types/categories';
+import { JobsExistResponse } from '../../../../common/types/job_service';
export const jobsApiProvider = (httpService: HttpService) => ({
jobsSummary(jobIds: string[]) {
@@ -138,9 +140,18 @@ export const jobsApiProvider = (httpService: HttpService) => ({
});
},
- jobsExist(jobIds: string[]) {
- const body = JSON.stringify({ jobIds });
- return httpService.http({
+ jobsExist(jobIds: string[], allSpaces: boolean = false) {
+ const body = JSON.stringify({ jobIds, allSpaces });
+ return httpService.http({
+ path: `${basePath()}/jobs/jobs_exist`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ jobsExist$(jobIds: string[], allSpaces: boolean = false): Observable {
+ const body = JSON.stringify({ jobIds, allSpaces });
+ return httpService.http$({
path: `${basePath()}/jobs/jobs_exist`,
method: 'POST',
body,
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts
index 67a95de3b3d71..c93af249be203 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts
+++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts
@@ -43,7 +43,7 @@ import { fieldsServiceProvider } from '../fields_service';
import { jobServiceProvider } from '../job_service';
import { resultsServiceProvider } from '../results_service';
import { JobExistResult, JobStat } from '../../../common/types/data_recognizer';
-import { MlJobsStatsResponse } from '../job_service/jobs';
+import { MlJobsStatsResponse } from '../../../common/types/job_service';
import { JobSavedObjectService } from '../../saved_objects';
const ML_DIR = 'ml';
@@ -533,7 +533,7 @@ export class DataRecognizer {
const jobInfo = await this._jobsService.jobsExist(jobIds);
// Check if the value for any of the jobs is false.
- const doJobsExist = Object.values(jobInfo).includes(false) === false;
+ const doJobsExist = Object.values(jobInfo).every((j) => j.exists === true);
results.jobsExist = doJobsExist;
if (doJobsExist === true) {
diff --git a/x-pack/plugins/ml/server/models/job_service/groups.ts b/x-pack/plugins/ml/server/models/job_service/groups.ts
index f6073ae7071b0..81b0494cbdf27 100644
--- a/x-pack/plugins/ml/server/models/job_service/groups.ts
+++ b/x-pack/plugins/ml/server/models/job_service/groups.ts
@@ -7,7 +7,7 @@
import { CalendarManager } from '../calendar';
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
import { Job } from '../../../common/types/anomaly_detection_jobs';
-import { MlJobsResponse } from './jobs';
+import { MlJobsResponse } from '../../../common/types/job_service';
import type { MlClient } from '../../lib/ml_client';
interface Group {
diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts
index 9abc34dfbb5d9..d47a1d4b4892d 100644
--- a/x-pack/plugins/ml/server/models/job_service/jobs.ts
+++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts
@@ -16,11 +16,14 @@ import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states';
import {
MlSummaryJob,
AuditMessage,
- Job,
- JobStats,
DatafeedWithStats,
CombinedJobWithStats,
} from '../../../common/types/anomaly_detection_jobs';
+import {
+ MlJobsResponse,
+ MlJobsStatsResponse,
+ JobsExistResponse,
+} from '../../../common/types/job_service';
import { GLOBAL_CALENDAR } from '../../../common/constants/calendars';
import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds';
import { jobAuditMessagesProvider } from '../job_audit_messages';
@@ -34,16 +37,6 @@ import {
import { groupsProvider } from './groups';
import type { MlClient } from '../../lib/ml_client';
-export interface MlJobsResponse {
- jobs: Job[];
- count: number;
-}
-
-export interface MlJobsStatsResponse {
- jobs: JobStats[];
- count: number;
-}
-
interface Results {
[id: string]: {
[status: string]: boolean;
@@ -420,10 +413,18 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
// Checks if each of the jobs in the specified list of IDs exist.
// Job IDs in supplied array may contain wildcard '*' characters
// e.g. *_low_request_rate_ecs
- async function jobsExist(jobIds: string[] = [], allSpaces: boolean = false) {
- const results: { [id: string]: boolean } = {};
+ async function jobsExist(
+ jobIds: string[] = [],
+ allSpaces: boolean = false
+ ): Promise {
+ const results: JobsExistResponse = {};
for (const jobId of jobIds) {
try {
+ if (jobId === '') {
+ results[jobId] = { exists: false, isGroup: false };
+ continue;
+ }
+
const { body } = allSpaces
? await client.asInternalUser.ml.getJobs({
job_id: jobId,
@@ -431,13 +432,15 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
: await mlClient.getJobs({
job_id: jobId,
});
- results[jobId] = body.count > 0;
+
+ const isGroup = body.jobs.some((j) => j.groups !== undefined && j.groups.includes(jobId));
+ results[jobId] = { exists: body.count > 0, isGroup };
} catch (e) {
// if a non-wildcarded job id is supplied, the get jobs endpoint will 404
if (e.statusCode !== 404) {
throw e;
}
- results[jobId] = false;
+ results[jobId] = { exists: false, isGroup: false };
}
}
return results;
diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts
index a196f1034fdd3..a153944f37bef 100644
--- a/x-pack/plugins/ml/server/models/results_service/results_service.ts
+++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts
@@ -17,7 +17,7 @@ import {
} from '../../../common/types/anomalies';
import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../common/constants/anomalies';
import { GetStoppedPartitionResult } from '../../../common/types/results';
-import { MlJobsResponse } from '../job_service/jobs';
+import { MlJobsResponse } from '../../../common/types/job_service';
import type { MlClient } from '../../lib/ml_client';
// Service for carrying out Elasticsearch queries to obtain data for the
diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts
index 0eb8f4abebf93..ca483ffa7bc1e 100644
--- a/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts
+++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts
@@ -17,6 +17,7 @@ export default ({ getService }: FtrProviderContext) => {
const jobIdSpace1 = 'fq_single_space1';
const jobIdSpace2 = 'fq_single_space2';
+ const groupSpace1 = 'farequote';
const idSpace1 = 'space1';
const idSpace2 = 'space2';
@@ -57,17 +58,25 @@ export default ({ getService }: FtrProviderContext) => {
it('should find single job from same space', async () => {
const body = await runRequest(idSpace1, 200, [jobIdSpace1]);
- expect(body).to.eql({ [jobIdSpace1]: true });
+ expect(body).to.eql({ [jobIdSpace1]: { exists: true, isGroup: false } });
+ });
+
+ it('should find single job from same space', async () => {
+ const body = await runRequest(idSpace1, 200, [groupSpace1]);
+ expect(body).to.eql({ [groupSpace1]: { exists: true, isGroup: true } });
});
it('should not find single job from different space', async () => {
const body = await runRequest(idSpace2, 200, [jobIdSpace1]);
- expect(body).to.eql({ [jobIdSpace1]: false });
+ expect(body).to.eql({ [jobIdSpace1]: { exists: false, isGroup: false } });
});
it('should only find job from same space when called with a list of jobs', async () => {
const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]);
- expect(body).to.eql({ [jobIdSpace1]: true, [jobIdSpace2]: false });
+ expect(body).to.eql({
+ [jobIdSpace1]: { exists: true, isGroup: false },
+ [jobIdSpace2]: { exists: false, isGroup: false },
+ });
});
});
};
From a0b787c39a7eab88c2de3a64aa872779b94abe39 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?=
Date: Fri, 15 Jan 2021 08:13:15 -0500
Subject: [PATCH 06/38] Fix flaky test for legacy authorization (#87642)
* Unskip test
* Increase attempts to 2 for retryIfConflicts
* Cleanup authorization for updateApiKey
---
.../plugins/alerts/server/alerts_client/alerts_client.ts | 5 +----
.../server/authorization/alerts_authorization.mock.ts | 1 -
.../alerts/server/authorization/alerts_authorization.ts | 7 +------
x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts | 4 +---
.../security_and_spaces/tests/alerting/rbac_legacy.ts | 2 +-
5 files changed, 4 insertions(+), 15 deletions(-)
diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
index b6606d9d7fe57..457079229de94 100644
--- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
@@ -817,10 +817,7 @@ export class AlertsClient {
attributes.consumer,
WriteOperations.UpdateApiKey
);
- if (
- attributes.actions.length &&
- !this.authorization.shouldUseLegacyAuthorization(attributes)
- ) {
+ if (attributes.actions.length) {
await this.actionsAuthorization.ensureAuthorized('execute');
}
} catch (error) {
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts
index 171e3978d0d0d..30de2c79732ce 100644
--- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts
@@ -14,7 +14,6 @@ const createAlertsAuthorizationMock = () => {
ensureAuthorized: jest.fn(),
filterByAlertTypeAuthorization: jest.fn(),
getFindAuthorizationFilter: jest.fn(),
- shouldUseLegacyAuthorization: jest.fn(),
};
return mocked;
};
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
index 6814e4ac1cc1b..29f2078bc61e4 100644
--- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
@@ -8,13 +8,12 @@ import Boom from '@hapi/boom';
import { map, mapValues, fromPairs, has } from 'lodash';
import { KibanaRequest } from 'src/core/server';
import { ALERTS_FEATURE_ID } from '../../common';
-import { AlertTypeRegistry, RawAlert } from '../types';
+import { AlertTypeRegistry } from '../types';
import { SecurityPluginSetup } from '../../../security/server';
import { RegistryAlertType } from '../alert_type_registry';
import { PluginStartContract as FeaturesPluginStart } from '../../../features/server';
import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger';
import { Space } from '../../../spaces/server';
-import { LEGACY_LAST_MODIFIED_VERSION } from '../saved_objects/migrations';
import { asFiltersByAlertTypeAndConsumer } from './alerts_authorization_kuery';
import { KueryNode } from '../../../../../src/plugins/data/server';
@@ -112,10 +111,6 @@ export class AlertsAuthorization {
);
}
- public shouldUseLegacyAuthorization(alert: RawAlert): boolean {
- return alert.meta?.versionApiKeyLastmodified === LEGACY_LAST_MODIFIED_VERSION;
- }
-
private shouldCheckAuthorization(): boolean {
return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
}
diff --git a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts
index 9cb1d7975855c..59ecc59ab57f8 100644
--- a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts
+++ b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts
@@ -15,9 +15,7 @@ import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server
type RetryableForConflicts = () => Promise;
// number of times to retry when conflicts occur
-// note: it seems unlikely that we'd need more than one retry, but leaving
-// this statically configurable in case we DO need > 1
-export const RetryForConflictsAttempts = 1;
+export const RetryForConflictsAttempts = 2;
// milliseconds to wait before retrying when conflicts occur
// note: we considered making this random, to help avoid a stampede, but
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
index 992d9210b9761..2b25c82cc92e5 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
@@ -62,7 +62,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
});
});
- it.skip('should schedule actions on legacy alerts', async () => {
+ it('should schedule actions on legacy alerts', async () => {
const reference = `alert:migrated-to-7.10:${user.username}`;
const migratedAlertId = MIGRATED_ALERT_ID[user.username];
From 5fd0027ba279fcc57f827d3580317c31f3c038dd Mon Sep 17 00:00:00 2001
From: Uladzislau Lasitsa
Date: Fri, 15 Jan 2021 16:45:26 +0300
Subject: [PATCH 07/38] use the same defaults as Lens (#87991)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/components/xy_settings.tsx | 36 +++++++++++++------
1 file changed, 25 insertions(+), 11 deletions(-)
diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx
index 3682fdf3350b0..18099173c56a9 100644
--- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx
+++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx
@@ -32,6 +32,8 @@ import {
LegendColorPicker,
TooltipProps,
TickFormatter,
+ VerticalAlignment,
+ HorizontalAlignment,
} from '@elastic/charts';
import { renderEndzoneTooltip } from '../../../charts/public';
@@ -70,6 +72,27 @@ type XYSettingsProps = Pick<
legendPosition: Position;
};
+function getValueLabelsStyling(isHorizontal: boolean) {
+ const VALUE_LABELS_MAX_FONTSIZE = 15;
+ const VALUE_LABELS_MIN_FONTSIZE = 10;
+ const VALUE_LABELS_VERTICAL_OFFSET = -10;
+ const VALUE_LABELS_HORIZONTAL_OFFSET = 10;
+
+ return {
+ displayValue: {
+ fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE },
+ fill: { textInverted: true, textBorder: 2 },
+ alignment: isHorizontal
+ ? {
+ vertical: VerticalAlignment.Middle,
+ }
+ : { horizontal: HorizontalAlignment.Center },
+ offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0,
+ offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET,
+ },
+ };
+}
+
export const XYSettings: FC = ({
markSizeRatio,
rotation,
@@ -92,10 +115,7 @@ export const XYSettings: FC = ({
const theme = themeService.useChartsTheme();
const baseTheme = themeService.useChartsBaseTheme();
const dimmingOpacity = getUISettings().get('visualization:dimmingOpacity');
- const fontSize =
- typeof theme.barSeriesStyle?.displayValue?.fontSize === 'number'
- ? { min: theme.barSeriesStyle?.displayValue?.fontSize }
- : theme.barSeriesStyle?.displayValue?.fontSize ?? { min: 8 };
+ const valueLabelsStyling = getValueLabelsStyling(rotation === 90 || rotation === -90);
const themeOverrides: PartialTheme = {
markSizeRatio,
@@ -105,13 +125,7 @@ export const XYSettings: FC = ({
},
},
barSeriesStyle: {
- displayValue: {
- fontSize,
- alignment: {
- horizontal: 'center',
- vertical: 'middle',
- },
- },
+ ...valueLabelsStyling,
},
axes: {
axisTitle: {
From 1bd408603243e80e5d8ed042505c4c87a140ffb8 Mon Sep 17 00:00:00 2001
From: Aleh Zasypkin
Date: Fri, 15 Jan 2021 14:55:53 +0100
Subject: [PATCH 08/38] Move Spaces, Security and EncryptedSavedObjects plugins
to separate TS projects (#88365)
---
.../encrypted_saved_objects/tsconfig.json | 14 ++++++
.../common/model/authenticated_user.ts | 2 +-
x-pack/plugins/security/tsconfig.json | 24 +++++++++
.../public/management/components/index.ts | 1 -
.../components/secure_space_message/index.ts | 7 ---
.../secure_space_message.tsx | 49 -------------------
.../enabled_features.test.tsx.snap | 20 --------
.../enabled_features.test.tsx | 8 ---
.../enabled_features/enabled_features.tsx | 24 +--------
.../edit_space/manage_space_page.test.tsx | 5 --
.../edit_space/manage_space_page.tsx | 12 +----
.../public/management/management_service.tsx | 6 +--
.../spaces_grid_pages.test.tsx.snap | 3 --
.../spaces_grid/spaces_grid_page.tsx | 10 +---
.../spaces_grid/spaces_grid_pages.test.tsx | 4 --
.../management/spaces_management_app.test.tsx | 15 ++----
.../management/spaces_management_app.tsx | 7 +--
x-pack/plugins/spaces/public/plugin.tsx | 4 --
x-pack/plugins/spaces/tsconfig.json | 22 +++++++++
.../translations/translations/ja-JP.json | 7 +--
.../translations/translations/zh-CN.json | 7 +--
x-pack/test/tsconfig.json | 3 ++
x-pack/tsconfig.json | 6 +++
x-pack/tsconfig.refs.json | 3 ++
24 files changed, 85 insertions(+), 178 deletions(-)
create mode 100644 x-pack/plugins/encrypted_saved_objects/tsconfig.json
create mode 100644 x-pack/plugins/security/tsconfig.json
delete mode 100644 x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts
delete mode 100644 x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx
create mode 100644 x-pack/plugins/spaces/tsconfig.json
diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json
new file mode 100644
index 0000000000000..2b51b313d34fc
--- /dev/null
+++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": ["server/**/*"],
+ "references": [
+ { "path": "../security/tsconfig.json" },
+ ]
+}
diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts
index 491ceb6845e28..5513fa27fa178 100644
--- a/x-pack/plugins/security/common/model/authenticated_user.ts
+++ b/x-pack/plugins/security/common/model/authenticated_user.ts
@@ -8,7 +8,7 @@ import type { AuthenticationProvider, User } from '.';
const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];
-interface UserRealm {
+export interface UserRealm {
name: string;
type: string;
}
diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json
new file mode 100644
index 0000000000000..6c3fd1851a8cb
--- /dev/null
+++ b/x-pack/plugins/security/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./target/types",
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "include": ["common/**/*", "public/**/*", "server/**/*"],
+ "references": [
+ { "path": "../features/tsconfig.json" },
+ { "path": "../licensing/tsconfig.json" },
+ { "path": "../spaces/tsconfig.json" },
+ { "path": "../task_manager/tsconfig.json" },
+ { "path": "../../../src/plugins/data/tsconfig.json" },
+ { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
+ { "path": "../../../src/plugins/home/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/management/tsconfig.json" },
+ { "path": "../../../src/plugins/security_oss/tsconfig.json" },
+ { "path": "../../../src/plugins/usage_collection/tsconfig.json" }
+ ]
+}
diff --git a/x-pack/plugins/spaces/public/management/components/index.ts b/x-pack/plugins/spaces/public/management/components/index.ts
index 7f9f80f470d12..91f4964e1da06 100644
--- a/x-pack/plugins/spaces/public/management/components/index.ts
+++ b/x-pack/plugins/spaces/public/management/components/index.ts
@@ -6,4 +6,3 @@
export { ConfirmDeleteModal } from './confirm_delete_modal';
export { UnauthorizedPrompt } from './unauthorized_prompt';
-export { SecureSpaceMessage } from './secure_space_message';
diff --git a/x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts b/x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts
deleted file mode 100644
index 4526dc791a224..0000000000000
--- a/x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export { SecureSpaceMessage } from './secure_space_message';
diff --git a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx b/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx
deleted file mode 100644
index 9500810a395f8..0000000000000
--- a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-import React, { Fragment } from 'react';
-import { ApplicationStart } from 'kibana/public';
-
-interface SecureSpaceMessageProps {
- getUrlForApp: ApplicationStart['getUrlForApp'];
-}
-
-export const SecureSpaceMessage = (props: SecureSpaceMessageProps) => {
- const rolesLinkTextAriaLabel = i18n.translate(
- 'xpack.spaces.management.secureSpaceMessage.rolesLinkTextAriaLabel',
- { defaultMessage: 'Roles management page' }
- );
- return (
-
-
-
-
@@ -97,6 +98,7 @@
data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}"
on-add-column="onAddColumn"
on-remove-column="onRemoveColumn"
+ use-new-fields-api="useNewFieldsApi"
>
diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts
index 735ee9f555740..2baf010b47c78 100644
--- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts
+++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts
@@ -48,6 +48,7 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) {
onMoveColumn: '=?',
onRemoveColumn: '=?',
inspectorAdapters: '=?',
+ useNewFieldsApi: '<',
},
link: ($scope: LazyScope, $el: JQuery) => {
$scope.persist = {
diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts
index 9bfba4de966be..cba50dfa58751 100644
--- a/src/plugins/discover/public/application/angular/helpers/index.ts
+++ b/src/plugins/discover/public/application/angular/helpers/index.ts
@@ -18,3 +18,4 @@
*/
export { buildPointSeriesData } from './point_series';
+export { formatRow } from './row_formatter';
diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts
new file mode 100644
index 0000000000000..60ee1e4c2b68b
--- /dev/null
+++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { formatRow } from './row_formatter';
+import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern';
+import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
+import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks';
+
+describe('Row formatter', () => {
+ const hit = {
+ foo: 'bar',
+ number: 42,
+ hello: '
'
+ );
+ });
+});
diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts
new file mode 100644
index 0000000000000..4ad50ef7621c5
--- /dev/null
+++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { template } from 'lodash';
+import { IndexPattern } from '../../../kibana_services';
+
+function noWhiteSpace(html: string) {
+ const TAGS_WITH_WS = />\s+<');
+}
+
+const templateHtml = `
+
- {!details.error && (
-
-
- {!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
- onAddFilter('_exists_', field.name, '+')}>
- {' '}
- {details.exists}
-
- ) : (
- {details.exists}
- )}{' '}
- / {details.total}{' '}
-
-
-
+ {!details.error && showFooter && (
+
)}
>
);
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx
new file mode 100644
index 0000000000000..028187569e977
--- /dev/null
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx
@@ -0,0 +1,82 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { findTestSubject } from '@elastic/eui/lib/test';
+// @ts-ignore
+import stubbedLogstashFields from 'fixtures/logstash_fields';
+import { mountWithIntl } from '@kbn/test/jest';
+import { coreMock } from '../../../../../../core/public/mocks';
+import { IndexPatternField } from '../../../../../data/public';
+import { getStubIndexPattern } from '../../../../../data/public/test_utils';
+import { DiscoverFieldDetailsFooter } from './discover_field_details_footer';
+
+const indexPattern = getStubIndexPattern(
+ 'logstash-*',
+ (cfg: any) => cfg,
+ 'time',
+ stubbedLogstashFields(),
+ coreMock.createSetup()
+);
+
+describe('discover sidebar field details footer', function () {
+ const onAddFilter = jest.fn();
+ const defaultProps = {
+ indexPattern,
+ details: { buckets: [], error: '', exists: 1, total: 2, columns: [] },
+ onAddFilter,
+ };
+
+ function mountComponent(field: IndexPatternField) {
+ const compProps = { ...defaultProps, field };
+ return mountWithIntl();
+ }
+
+ it('renders properly', function () {
+ const visualizableField = new IndexPatternField({
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ });
+ const component = mountComponent(visualizableField);
+ expect(component).toMatchSnapshot();
+ });
+
+ it('click on addFilter calls the function', function () {
+ const visualizableField = new IndexPatternField({
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ });
+ const component = mountComponent(visualizableField);
+ const onAddButton = findTestSubject(component, 'onAddFilterButton');
+ onAddButton.simulate('click');
+ expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+');
+ });
+});
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx
new file mode 100644
index 0000000000000..58e91c85913a1
--- /dev/null
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx
@@ -0,0 +1,70 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { EuiLink, EuiPopoverFooter, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { IndexPatternField } from '../../../../../data/common/index_patterns/fields';
+import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
+import { FieldDetails } from './types';
+
+interface DiscoverFieldDetailsFooterProps {
+ field: IndexPatternField;
+ indexPattern: IndexPattern;
+ details: FieldDetails;
+ onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+}
+
+export function DiscoverFieldDetailsFooter({
+ field,
+ indexPattern,
+ details,
+ onAddFilter,
+}: DiscoverFieldDetailsFooterProps) {
+ return (
+
+
+ {!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
+ onAddFilter('_exists_', field.name, '+')}
+ data-test-subj="onAddFilterButton"
+ >
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
index 57cc45b3c3e9f..6c312924fb7b7 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx
@@ -100,6 +100,10 @@ export interface DiscoverSidebarProps {
* Callback function to select another index pattern
*/
setIndexPattern: (id: string) => void;
+ /**
+ * If on, fields are read from the fields API, not from source
+ */
+ useNewFieldsApi?: boolean;
/**
* Metric tracking function
* @param metricType
@@ -127,9 +131,11 @@ export function DiscoverSidebar({
setFieldFilter,
setIndexPattern,
trackUiMetric,
+ useNewFieldsApi = false,
useFlyout = false,
}: DiscoverSidebarProps) {
const [fields, setFields] = useState(null);
+
useEffect(() => {
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
setFields(newFields);
@@ -154,13 +160,10 @@ export function DiscoverSidebar({
selected: selectedFields,
popular: popularFields,
unpopular: unpopularFields,
- } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter), [
- fields,
- columns,
- popularLimit,
- fieldCounts,
- fieldFilter,
- ]);
+ } = useMemo(
+ () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi),
+ [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi]
+ );
const fieldTypes = useMemo(() => {
const result = ['any'];
@@ -174,6 +177,27 @@ export function DiscoverSidebar({
return result;
}, [fields]);
+ const multiFields = useMemo(() => {
+ if (!useNewFieldsApi || !fields) {
+ return undefined;
+ }
+ const map = new Map>();
+ fields.forEach((field) => {
+ const parent = field.spec?.subType?.multi?.parent;
+ if (!parent) {
+ return;
+ }
+ const multiField = {
+ field,
+ isSelected: selectedFields.includes(field),
+ };
+ const value = map.get(parent) ?? [];
+ value.push(multiField);
+ map.set(parent, value);
+ });
+ return map;
+ }, [fields, useNewFieldsApi, selectedFields]);
+
if (!selectedIndexPattern || !fields) {
return null;
}
@@ -278,6 +302,7 @@ export function DiscoverSidebar({
getDetails={getDetailsByField}
selected={true}
trackUiMetric={trackUiMetric}
+ multiFields={multiFields?.get(field.name)}
/>
);
@@ -338,6 +363,7 @@ export function DiscoverSidebar({
onAddFilter={onAddFilter}
getDetails={getDetailsByField}
trackUiMetric={trackUiMetric}
+ multiFields={multiFields?.get(field.name)}
/>
);
@@ -366,6 +392,7 @@ export function DiscoverSidebar({
onAddFilter={onAddFilter}
getDetails={getDetailsByField}
trackUiMetric={trackUiMetric}
+ multiFields={multiFields?.get(field.name)}
/>
);
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx
index 0413ebd17d71b..3000291fc23bb 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx
@@ -103,6 +103,10 @@ export interface DiscoverSidebarResponsiveProps {
* Shows index pattern and a button that displays the sidebar in a flyout
*/
useFlyout?: boolean;
+ /**
+ * Read from the Fields API
+ */
+ useNewFieldsApi?: boolean;
}
/**
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts
index 22cacae4c3b45..6cbfa03a070db 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts
+++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts
@@ -69,7 +69,8 @@ describe('group_fields', function () {
['currency'],
5,
fieldCounts,
- fieldFilterState
+ fieldFilterState,
+ false
);
expect(actual).toMatchInlineSnapshot(`
Object {
@@ -118,6 +119,80 @@ describe('group_fields', function () {
}
`);
});
+ it('should group fields in selected, popular, unpopular group if they contain multifields', function () {
+ const category = {
+ name: 'category',
+ type: 'string',
+ esTypes: ['text'],
+ count: 1,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ };
+ const currency = {
+ name: 'currency',
+ displayName: 'currency',
+ kbnFieldType: {
+ esTypes: ['string', 'text', 'keyword', '_type', '_id'],
+ filterable: true,
+ name: 'string',
+ sortable: true,
+ },
+ spec: {
+ esTypes: ['text'],
+ name: 'category',
+ },
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ };
+ const currencyKeyword = {
+ name: 'currency.keyword',
+ displayName: 'currency.keyword',
+ type: 'string',
+ esTypes: ['keyword'],
+ kbnFieldType: {
+ esTypes: ['string', 'text', 'keyword', '_type', '_id'],
+ filterable: true,
+ name: 'string',
+ sortable: true,
+ },
+ spec: {
+ aggregatable: true,
+ esTypes: ['keyword'],
+ name: 'category.keyword',
+ readFromDocValues: true,
+ searchable: true,
+ shortDotsEnable: false,
+ subType: {
+ multi: {
+ parent: 'currency',
+ },
+ },
+ },
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: false,
+ };
+ const fieldsToGroup = [category, currency, currencyKeyword];
+
+ const fieldFilterState = getDefaultFieldFilter();
+
+ const actual = groupFields(
+ fieldsToGroup as any,
+ ['currency'],
+ 5,
+ fieldCounts,
+ fieldFilterState,
+ true
+ );
+ expect(actual.popular).toEqual([category]);
+ expect(actual.selected).toEqual([currency]);
+ expect(actual.unpopular).toEqual([]);
+ });
it('should sort selected fields by columns order ', function () {
const fieldFilterState = getDefaultFieldFilter();
@@ -127,7 +202,8 @@ describe('group_fields', function () {
['customer_birth_date', 'currency', 'unknown'],
5,
fieldCounts,
- fieldFilterState
+ fieldFilterState,
+ false
);
expect(actual1.selected.map((field) => field.name)).toEqual([
'customer_birth_date',
@@ -140,7 +216,8 @@ describe('group_fields', function () {
['currency', 'customer_birth_date', 'unknown'],
5,
fieldCounts,
- fieldFilterState
+ fieldFilterState,
+ false
);
expect(actual2.selected.map((field) => field.name)).toEqual([
'currency',
diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx
index c34becc97cb93..e6c3d0fe3ea42 100644
--- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx
@@ -33,7 +33,8 @@ export function groupFields(
columns: string[],
popularLimit: number,
fieldCounts: Record,
- fieldFilterState: FieldFilterState
+ fieldFilterState: FieldFilterState,
+ useNewFieldsApi: boolean
): GroupedFields {
const result: GroupedFields = {
selected: [],
@@ -62,12 +63,17 @@ export function groupFields(
if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) {
continue;
}
+ const isSubfield = useNewFieldsApi && field.spec?.subType?.multi?.parent;
if (columns.includes(field.name)) {
result.selected.push(field);
} else if (popular.includes(field.name) && field.type !== '_source') {
- result.popular.push(field);
+ if (!isSubfield) {
+ result.popular.push(field);
+ }
} else if (field.type !== '_source') {
- result.unpopular.push(field);
+ if (!isSubfield) {
+ result.unpopular.push(field);
+ }
}
}
// add columns, that are not part of the index pattern, to be removeable
diff --git a/src/plugins/discover/public/application/components/sidebar/types.ts b/src/plugins/discover/public/application/components/sidebar/types.ts
index d80662b65cc7b..4ec731e852ce3 100644
--- a/src/plugins/discover/public/application/components/sidebar/types.ts
+++ b/src/plugins/discover/public/application/components/sidebar/types.ts
@@ -25,7 +25,7 @@ export interface IndexPatternRef {
export interface FieldDetails {
error: string;
exists: number;
- total: boolean;
+ total: number;
buckets: Bucket[];
columns: string[];
}
diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts
index 458b9b7e066fd..5af4449a63e0b 100644
--- a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts
+++ b/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts
@@ -29,7 +29,8 @@ export function getSwitchIndexPatternAppState(
nextIndexPattern: IndexPattern,
currentColumns: string[],
currentSort: SortPairArr[],
- modifyColumns: boolean = true
+ modifyColumns: boolean = true,
+ useNewFieldsApi: boolean = false
) {
const nextColumns = modifyColumns
? currentColumns.filter(
@@ -38,9 +39,11 @@ export function getSwitchIndexPatternAppState(
)
: currentColumns;
const nextSort = getSortArray(currentSort, nextIndexPattern);
+ const defaultColumns = useNewFieldsApi ? [] : ['_source'];
+ const columns = nextColumns.length ? nextColumns : defaultColumns;
return {
index: nextIndexPattern.id,
- columns: nextColumns.length ? nextColumns : ['_source'],
+ columns,
sort: nextSort,
};
}
diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts
index 8ec2012b5843e..2f373c34eb17d 100644
--- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts
+++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts
@@ -49,6 +49,8 @@ export async function persistSavedSearch(
indexPattern,
services,
sort: state.sort as SortOrder[],
+ columns: state.columns || [],
+ useNewFieldsApi: false,
});
savedSearch.columns = state.columns || [];
diff --git a/src/plugins/discover/public/application/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/helpers/update_search_source.test.ts
index 91832325432ef..615a414680469 100644
--- a/src/plugins/discover/public/application/helpers/update_search_source.test.ts
+++ b/src/plugins/discover/public/application/helpers/update_search_source.test.ts
@@ -44,8 +44,37 @@ describe('updateSearchSource', () => {
} as unknown) as IUiSettingsClient,
} as unknown) as DiscoverServices,
sort: [] as SortOrder[],
+ columns: [],
+ useNewFieldsApi: false,
});
expect(result.getField('index')).toEqual(indexPatternMock);
expect(result.getField('size')).toEqual(sampleSize);
+ expect(result.getField('fields')).toBe(undefined);
+ });
+
+ test('updates a given search source with the usage of the new fields api', async () => {
+ const searchSourceMock = createSearchSourceMock({});
+ const sampleSize = 250;
+ const result = updateSearchSource(searchSourceMock, {
+ indexPattern: indexPatternMock,
+ services: ({
+ data: dataPluginMock.createStartContract(),
+ uiSettings: ({
+ get: (key: string) => {
+ if (key === SAMPLE_SIZE_SETTING) {
+ return sampleSize;
+ }
+ return false;
+ },
+ } as unknown) as IUiSettingsClient,
+ } as unknown) as DiscoverServices,
+ sort: [] as SortOrder[],
+ columns: [],
+ useNewFieldsApi: true,
+ });
+ expect(result.getField('index')).toEqual(indexPatternMock);
+ expect(result.getField('size')).toEqual(sampleSize);
+ expect(result.getField('fields')).toEqual(['*']);
+ expect(result.getField('fieldsFromSource')).toBe(undefined);
});
});
diff --git a/src/plugins/discover/public/application/helpers/update_search_source.ts b/src/plugins/discover/public/application/helpers/update_search_source.ts
index 324dc8a48457a..46f1c9f626054 100644
--- a/src/plugins/discover/public/application/helpers/update_search_source.ts
+++ b/src/plugins/discover/public/application/helpers/update_search_source.ts
@@ -31,10 +31,14 @@ export function updateSearchSource(
indexPattern,
services,
sort,
+ columns,
+ useNewFieldsApi,
}: {
indexPattern: IndexPattern;
services: DiscoverServices;
sort: SortOrder[];
+ columns: string[];
+ useNewFieldsApi: boolean;
}
) {
const { uiSettings, data } = services;
@@ -50,5 +54,13 @@ export function updateSearchSource(
.setField('sort', usedSort)
.setField('query', data.query.queryString.getQuery() || null)
.setField('filter', data.query.filterManager.getFilters());
+ if (useNewFieldsApi) {
+ searchSource.removeField('fieldsFromSource');
+ searchSource.setField('fields', ['*']);
+ } else {
+ searchSource.removeField('fields');
+ const fieldNames = indexPattern.fields.map((field) => field.name);
+ searchSource.setField('fieldsFromSource', fieldNames);
+ }
return searchSource;
}
diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts
index 425928385e64a..673f55c78a506 100644
--- a/src/plugins/discover/server/ui_settings.ts
+++ b/src/plugins/discover/server/ui_settings.ts
@@ -35,6 +35,7 @@ import {
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
DOC_TABLE_LEGACY,
MODIFY_COLUMNS_ON_SWITCH,
+ SEARCH_FIELDS_FROM_SOURCE,
} from '../common';
export const uiSettings: Record = {
@@ -198,4 +199,11 @@ export const uiSettings: Record = {
name: 'discover:modifyColumnsOnSwitchTitle',
},
},
+ [SEARCH_FIELDS_FROM_SOURCE]: {
+ name: 'Read fields from _source',
+ description: `When enabled will load documents directly from \`_source\`. This is soon going to be deprecated. When disabled, will retrieve fields via the new Fields API in the high-level search service.`,
+ value: false,
+ category: ['discover'],
+ schema: schema.boolean(),
+ },
};
diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js
index 8fe08d13af0aa..8772b10a4b8c8 100644
--- a/test/functional/apps/context/_date_nanos_custom_timestamp.js
+++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js
@@ -38,6 +38,7 @@ export default function ({ getService, getPageObjects }) {
await kibanaServer.uiSettings.update({
'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`,
'context:step': `${TEST_STEP_SIZE}`,
+ 'discover:searchFieldsFromSource': true,
});
});
diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts
index 92d9893cab0b6..97b8eb564a256 100644
--- a/test/functional/apps/discover/_data_grid_doc_navigation.ts
+++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts
@@ -56,7 +56,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(hasDocHit).to.be(true);
});
- it('add filter should create an exists filter if value is null (#7189)', async function () {
+ // no longer relevant as null field won't be returned in the Fields API response
+ xit('add filter should create an exists filter if value is null (#7189)', async function () {
await PageObjects.discover.waitUntilSearchingHasFinished();
// Filter special document
await filterBar.addFilter('agent', 'is', 'Missing/Fields');
diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts
index 8224f59f7fabf..137c19149d274 100644
--- a/test/functional/apps/discover/_data_grid_field_data.ts
+++ b/test/functional/apps/discover/_data_grid_field_data.ts
@@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('the search term should be highlighted in the field data', async function () {
// marks is the style that highlights the text in yellow
const marks = await PageObjects.discover.getMarks();
- expect(marks.length).to.be(25);
+ expect(marks.length).to.be(50);
expect(marks.indexOf('php')).to.be(0);
});
diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts
new file mode 100644
index 0000000000000..94cb4ed5fa52e
--- /dev/null
+++ b/test/functional/apps/discover/_discover_fields_api.ts
@@ -0,0 +1,71 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import expect from '@kbn/expect';
+import { FtrProviderContext } from './ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const log = getService('log');
+ const retry = getService('retry');
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ 'discover:searchFieldsFromSource': false,
+ };
+ describe('discover uses fields API test', function describeIndexTests() {
+ before(async function () {
+ log.debug('load kibana index with default index pattern');
+ await esArchiver.load('discover');
+ await esArchiver.loadIfNeeded('logstash_functional');
+ await kibanaServer.uiSettings.replace(defaultSettings);
+ log.debug('discover');
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ });
+
+ after(async () => {
+ await kibanaServer.uiSettings.replace({ 'discover:searchFieldsFromSource': true });
+ });
+
+ it('should correctly display documents', async function () {
+ log.debug('check if Document title exists in the grid');
+ expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
+ const rowData = await PageObjects.discover.getDocTableIndex(1);
+ log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)');
+ expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok();
+ const expectedHitCount = '14,004';
+ await retry.try(async function () {
+ expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount);
+ });
+ });
+
+ it('adding a column removes a default column', async function () {
+ await PageObjects.discover.clickFieldListItemAdd('_score');
+ expect(await PageObjects.discover.getDocHeader()).to.have.string('_score');
+ expect(await PageObjects.discover.getDocHeader()).not.to.have.string('Document');
+ });
+
+ it('removing a column adds a default column', async function () {
+ await PageObjects.discover.clickFieldListItemRemove('_score');
+ expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score');
+ expect(await PageObjects.discover.getDocHeader()).to.have.string('Document');
+ });
+ });
+}
diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts
index 76612b255ac23..79632942cf04a 100644
--- a/test/functional/apps/discover/_doc_navigation.ts
+++ b/test/functional/apps/discover/_doc_navigation.ts
@@ -55,7 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(hasDocHit).to.be(true);
});
- it('add filter should create an exists filter if value is null (#7189)', async function () {
+ // no longer relevant as null field won't be returned in the Fields API response
+ xit('add filter should create an exists filter if value is null (#7189)', async function () {
await PageObjects.discover.waitUntilSearchingHasFinished();
// Filter special document
await filterBar.addFilter('agent', 'is', 'Missing/Fields');
diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts
index e08325a81a3e8..3811cde8a6367 100644
--- a/test/functional/apps/discover/_field_data.ts
+++ b/test/functional/apps/discover/_field_data.ts
@@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await esArchiver.load('discover');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
+ 'discover:searchFieldsFromSource': true,
});
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
await PageObjects.common.navigateToApp('discover');
diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts
new file mode 100644
index 0000000000000..923a021f5fad6
--- /dev/null
+++ b/test/functional/apps/discover/_field_data_with_fields_api.ts
@@ -0,0 +1,105 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const retry = getService('retry');
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const toasts = getService('toasts');
+ const queryBar = getService('queryBar');
+ const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']);
+
+ describe('discover tab with new fields API', function describeIndexTests() {
+ this.tags('includeFirefox');
+ before(async function () {
+ await esArchiver.loadIfNeeded('logstash_functional');
+ await esArchiver.load('discover');
+ await kibanaServer.uiSettings.replace({
+ defaultIndex: 'logstash-*',
+ 'discover:searchFieldsFromSource': false,
+ });
+ await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
+ await PageObjects.common.navigateToApp('discover');
+ });
+ describe('field data', function () {
+ it('search php should show the correct hit count', async function () {
+ const expectedHitCount = '445';
+ await retry.try(async function () {
+ await queryBar.setQuery('php');
+ await queryBar.submitQuery();
+ const hitCount = await PageObjects.discover.getHitCount();
+ expect(hitCount).to.be(expectedHitCount);
+ });
+ });
+
+ it('the search term should be highlighted in the field data', async function () {
+ // marks is the style that highlights the text in yellow
+ const marks = await PageObjects.discover.getMarks();
+ expect(marks.length).to.be(100);
+ expect(marks.indexOf('php')).to.be(0);
+ });
+
+ it('search type:apache should show the correct hit count', async function () {
+ const expectedHitCount = '11,156';
+ await queryBar.setQuery('type:apache');
+ await queryBar.submitQuery();
+ await retry.try(async function tryingForTime() {
+ const hitCount = await PageObjects.discover.getHitCount();
+ expect(hitCount).to.be(expectedHitCount);
+ });
+ });
+
+ it('doc view should show Time and Document columns', async function () {
+ const expectedHeader = 'Time Document';
+ const Docheader = await PageObjects.discover.getDocHeader();
+ expect(Docheader).to.be(expectedHeader);
+ });
+
+ it('doc view should sort ascending', async function () {
+ const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000';
+ await PageObjects.discover.clickDocSortDown();
+
+ // we don't technically need this sleep here because the tryForTime will retry and the
+ // results will match on the 2nd or 3rd attempt, but that debug output is huge in this
+ // case and it can be avoided with just a few seconds sleep.
+ await PageObjects.common.sleep(2000);
+ await retry.try(async function tryingForTime() {
+ const rowData = await PageObjects.discover.getDocTableIndex(1);
+
+ expect(rowData.startsWith(expectedTimeStamp)).to.be.ok();
+ });
+ });
+
+ it('a bad syntax query should show an error message', async function () {
+ const expectedError =
+ 'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
+ 'whitespace but "(" found.';
+ await queryBar.setQuery('xxx(yyy))');
+ await queryBar.submitQuery();
+ const { message } = await toasts.getErrorToast();
+ expect(message).to.contain(expectedError);
+ await toasts.dismissToast();
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts
index fe5613a4e3f19..e8ad6131bcc21 100644
--- a/test/functional/apps/discover/_large_string.ts
+++ b/test/functional/apps/discover/_large_string.ts
@@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('verify the large string book present', async function () {
const ExpectedDoc =
- 'mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' +
+ '_id:1 _type: - _index:testlargestring _score:0' +
+ ' mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' +
' This eBook is for the use of anyone anywhere in the United States' +
' and most other parts of the world at no cost and with almost no restrictions whatsoever.' +
' You may copy it, give it away or re-use it under the terms of the' +
diff --git a/test/functional/apps/discover/_shared_links.ts b/test/functional/apps/discover/_shared_links.ts
index 51ea5f997e859..b15f5b0aae39f 100644
--- a/test/functional/apps/discover/_shared_links.ts
+++ b/test/functional/apps/discover/_shared_links.ts
@@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'/app/discover?_t=1453775307251#' +
'/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' +
":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" +
- "-23T18:31:44.000Z'))&_a=(columns:!(_source),filters:!(),index:'logstash-" +
+ "-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-" +
"*',interval:auto,query:(language:kuery,query:'')" +
",sort:!(!('@timestamp',desc)))";
const actualUrl = await PageObjects.share.getSharedUrl();
diff --git a/test/functional/apps/discover/_source_filters.ts b/test/functional/apps/discover/_source_filters.ts
index 0af7c0ade79ba..d2ae02ef25de4 100644
--- a/test/functional/apps/discover/_source_filters.ts
+++ b/test/functional/apps/discover/_source_filters.ts
@@ -40,6 +40,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// and load a set of makelogs data
await esArchiver.loadIfNeeded('logstash_functional');
+ await kibanaServer.uiSettings.update({
+ 'discover:searchFieldsFromSource': true,
+ });
+
log.debug('discover');
await PageObjects.common.navigateToApp('discover');
diff --git a/test/functional/apps/discover/ftr_provider_context.d.ts b/test/functional/apps/discover/ftr_provider_context.d.ts
new file mode 100644
index 0000000000000..a4894e024b612
--- /dev/null
+++ b/test/functional/apps/discover/ftr_provider_context.d.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { GenericFtrProviderContext } from '@kbn/test/types/ftr';
+import { services } from '../../services';
+import { pageObjects } from '../../page_objects';
+
+export type FtrProviderContext = GenericFtrProviderContext;
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index 450049af66abf..5fd49a1d35216 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -42,6 +42,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_filter_editor'));
loadTestFile(require.resolve('./_errors'));
loadTestFile(require.resolve('./_field_data'));
+ loadTestFile(require.resolve('./_field_data_with_fields_api'));
loadTestFile(require.resolve('./_shared_links'));
loadTestFile(require.resolve('./_sidebar'));
loadTestFile(require.resolve('./_source_filters'));
@@ -51,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_date_nanos'));
loadTestFile(require.resolve('./_date_nanos_mixed'));
loadTestFile(require.resolve('./_indexpattern_without_timefield'));
+ loadTestFile(require.resolve('./_discover_fields_api'));
loadTestFile(require.resolve('./_data_grid'));
loadTestFile(require.resolve('./_data_grid_context'));
loadTestFile(require.resolve('./_data_grid_field_data'));
diff --git a/test/functional/fixtures/es_archiver/date_nanos/mappings.json b/test/functional/fixtures/es_archiver/date_nanos/mappings.json
index bea82767f6cbb..f9ef429a0f97c 100644
--- a/test/functional/fixtures/es_archiver/date_nanos/mappings.json
+++ b/test/functional/fixtures/es_archiver/date_nanos/mappings.json
@@ -5,7 +5,8 @@
"mappings": {
"properties": {
"@timestamp": {
- "type": "date_nanos"
+ "type": "date_nanos",
+ "format": "strict_date_optional_time_nanos"
}
}
},
diff --git a/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json b/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json
index c62918abced58..b29f6b111b06d 100644
--- a/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json
+++ b/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json
@@ -29,7 +29,8 @@
"mappings": {
"properties": {
"timestamp": {
- "type": "date_nanos"
+ "type": "date_nanos",
+ "format": "strict_date_optional_time_nanos"
}
}
},
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index f479b3a521185..999da541615c1 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -1510,10 +1510,8 @@
"discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
"discover.embeddable.search.displayName": "検索",
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
- "discover.fieldChooser.detailViews.existsText": "存在する",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"",
- "discover.fieldChooser.detailViews.recordsText": "記録",
"discover.fieldChooser.detailViews.visualizeLinkText": "可視化",
"discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}を表に追加",
"discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index a661537e6e288..d92a2ba94ff34 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -1510,10 +1510,8 @@
"discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。",
"discover.embeddable.search.displayName": "搜索",
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
- "discover.fieldChooser.detailViews.existsText": "存在于",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”",
- "discover.fieldChooser.detailViews.recordsText": "个记录",
"discover.fieldChooser.detailViews.visualizeLinkText": "可视化",
"discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中",
"discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列",
diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js
index 72f463be48fd5..0595322ad2d21 100644
--- a/x-pack/test/functional/apps/security/doc_level_security_roles.js
+++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js
@@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }) {
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
expect(rowData).to.be(
- 'name:ABC Company region:EAST _id:doc1 _type: - _index:dlstest _score:0'
+ '_id:doc1 _type: - _index:dlstest _score:0 region.keyword:EAST name:ABC Company name.keyword:ABC Company region:EAST'
);
});
after('logout', async () => {
diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js
index 7b22d72885c9d..3f3984dd05a94 100644
--- a/x-pack/test/functional/apps/security/field_level_security.js
+++ b/x-pack/test/functional/apps/security/field_level_security.js
@@ -112,7 +112,7 @@ export default function ({ getService, getPageObjects }) {
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
expect(rowData).to.be(
- 'customer_ssn:444.555.6666 customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0'
+ '_id:2 _type: - _index:flstest _score:0 customer_name.keyword:ABC Company customer_ssn:444.555.6666 customer_region.keyword:WEST runtime_customer_ssn:444.555.6666 calculated at runtime customer_region:WEST customer_name:ABC Company customer_ssn.keyword:444.555.6666'
);
});
@@ -126,7 +126,7 @@ export default function ({ getService, getPageObjects }) {
});
const rowData = await PageObjects.discover.getDocTableIndex(1);
expect(rowData).to.be(
- 'customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0'
+ '_id:2 _type: - _index:flstest _score:0 customer_name.keyword:ABC Company customer_region.keyword:WEST customer_region:WEST customer_name:ABC Company'
);
});
diff --git a/x-pack/test/functional/es_archives/security/flstest/data/mappings.json b/x-pack/test/functional/es_archives/security/flstest/data/mappings.json
index 4f419e4b6ade4..0b970d5a3c1df 100644
--- a/x-pack/test/functional/es_archives/security/flstest/data/mappings.json
+++ b/x-pack/test/functional/es_archives/security/flstest/data/mappings.json
@@ -7,7 +7,8 @@
"runtime_customer_ssn": {
"type": "keyword",
"script": {
- "source": "emit(doc['customer_ssn'].value + ' calculated at runtime')"
+ "lang": "painless",
+ "source": "if (doc['customer_ssn'].size() !== 0) { return emit(doc['customer_ssn'].value + ' calculated at runtime') }"
}
}
},
@@ -37,7 +38,8 @@
"type": "keyword"
}
},
- "type": "text"
+ "type": "text",
+ "fielddata": true
}
}
},
From fc370b74eb2f5efc0fe58ef587371fc276d54bd6 Mon Sep 17 00:00:00 2001
From: Justin Kambic
Date: Fri, 15 Jan 2021 10:25:06 -0500
Subject: [PATCH 12/38] [Uptime] Unskip "Observer location" test block (#87571)
* Unskip "Observer location" test block.
* Commit temp "describe.only" to make flaky test runner go faster.
* Add optional chain for some potentially-null props.
* Make overview filters type partial.
* Repair broken types.
* Remove \`only\` call from test.
---
.../overview_filters/overview_filters.ts | 2 +-
.../alerts_containers/alert_monitor_status.tsx | 2 +-
.../overview/filter_group/filter_group.tsx | 18 +++++++++++-------
.../test/functional/apps/uptime/locations.ts | 3 +--
4 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts
index 9b9241494f001..854b4b6b6e9b8 100644
--- a/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts
+++ b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts
@@ -6,7 +6,7 @@
import * as t from 'io-ts';
-export const OverviewFiltersType = t.type({
+export const OverviewFiltersType = t.partial({
locations: t.array(t.string),
ports: t.array(t.number),
schemes: t.array(t.string),
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx
index 50b6fe2aa0ef1..1c1deb2104970 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx
@@ -122,7 +122,7 @@ export const AlertMonitorStatus: React.FC = ({
enabled={enabled}
hasFilters={!!overviewFilters?.filters}
isOldAlert={isOldAlert}
- locations={locations}
+ locations={locations || []}
numTimes={numTimes}
setAlertParams={setAlertParams}
shouldUpdateUrl={shouldUpdateUrl}
diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx
index 45268977a543f..a5256ee1e2626 100644
--- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx
@@ -24,6 +24,10 @@ const Container = styled(EuiFilterGroup)`
margin-bottom: 10px;
`;
+function isDisabled(array?: T[]) {
+ return array ? array.length === 0 : true;
+}
+
export const FilterGroupComponent: React.FC = ({
overviewFilters,
loading,
@@ -51,7 +55,7 @@ export const FilterGroupComponent: React.FC = ({
onFilterFieldChange,
fieldName: 'observer.geo.name',
id: 'location',
- items: locations,
+ items: locations || [],
selectedItems: selectedLocations,
title: filterLabels.LOCATION,
},
@@ -63,8 +67,8 @@ export const FilterGroupComponent: React.FC = ({
onFilterFieldChange,
fieldName: 'url.port',
id: 'port',
- disabled: ports.length === 0,
- items: ports.map((p: number) => p.toString()),
+ disabled: isDisabled(ports),
+ items: ports?.map((p: number) => p.toString()) ?? [],
selectedItems: selectedPorts,
title: filterLabels.PORT,
},
@@ -73,8 +77,8 @@ export const FilterGroupComponent: React.FC = ({
onFilterFieldChange,
fieldName: 'monitor.type',
id: 'scheme',
- disabled: schemes.length === 0,
- items: schemes,
+ disabled: isDisabled(schemes),
+ items: schemes ?? [],
selectedItems: selectedSchemes,
title: filterLabels.SCHEME,
},
@@ -83,8 +87,8 @@ export const FilterGroupComponent: React.FC = ({
onFilterFieldChange,
fieldName: 'tags',
id: 'tags',
- disabled: tags.length === 0,
- items: tags,
+ disabled: isDisabled(tags),
+ items: tags ?? [],
selectedItems: selectedTags,
title: filterLabels.TAGS,
},
diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts
index eb5a642c8d69d..6bfa19c6ef578 100644
--- a/x-pack/test/functional/apps/uptime/locations.ts
+++ b/x-pack/test/functional/apps/uptime/locations.ts
@@ -38,8 +38,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'down');
};
- // FLAKY: https://github.com/elastic/kibana/issues/85208
- describe.skip('Observer location', () => {
+ describe('Observer location', () => {
const start = '~ 15 minutes ago';
const end = 'now';
From e2fc156bc55b9be2ccfa8715b4637ef8401fb4bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Fri, 15 Jan 2021 17:03:56 +0100
Subject: [PATCH 13/38] [APM] Extracting custom link flaky test. (#88455)
* extracting the flaky test to another file
* extracting the flaky test to another file
---
.../DeleteButton.test.tsx | 29 +++++++++++++++++
.../CustomizeUI/CustomLink/index.test.tsx | 32 +------------------
2 files changed, 30 insertions(+), 31 deletions(-)
create mode 100644 x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.test.tsx
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.test.tsx
new file mode 100644
index 0000000000000..1dda204c8d6bc
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/DeleteButton.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { fireEvent, render } from '@testing-library/react';
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { MockApmPluginContextWrapper } from '../../../../../../context/apm_plugin/mock_apm_plugin_context';
+import * as apmApi from '../../../../../../services/rest/createCallApmApi';
+import { DeleteButton } from './DeleteButton';
+
+describe('Delete custom link', () => {
+ beforeAll(() => {
+ jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({});
+ });
+ it('deletes a custom link', async () => {
+ const onDeleteMock = jest.fn();
+ const { getByText } = render(
+
+
+
+ );
+ await act(async () => {
+ fireEvent.click(getByText('Delete'));
+ });
+ expect(onDeleteMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
index 63dd486544124..c476686595b39 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
@@ -4,12 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- fireEvent,
- render,
- RenderResult,
- waitFor,
-} from '@testing-library/react';
+import { fireEvent, render, RenderResult } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { CustomLinkOverview } from '.';
@@ -219,31 +214,6 @@ describe('CustomLink', () => {
expect(saveCustomLinkSpy).toHaveBeenCalledTimes(1);
});
- // FLAKY: https://github.com/elastic/kibana/issues/75106
- it.skip('deletes a custom link', async () => {
- const mockContext = getMockAPMContext({ canSave: true });
- const component = render(
-
-
-
-
-
- );
- expect(component.queryByText('Create link')).not.toBeInTheDocument();
- const editButtons = component.getAllByLabelText('Edit');
- expect(editButtons.length).toEqual(2);
- act(() => {
- fireEvent.click(editButtons[0]);
- });
- await waitFor(() =>
- expect(component.queryByText('Create link')).toBeInTheDocument()
- );
- await act(async () => {
- fireEvent.click(component.getByText('Delete'));
- });
- expect(refetch).toHaveBeenCalled();
- });
-
describe('Filters', () => {
const addFilterField = (component: RenderResult, amount: number) => {
for (let i = 1; i <= amount; i++) {
From 329a5a3f21e8f6ef86e0139baf090f6ddc8834b4 Mon Sep 17 00:00:00 2001
From: Jason Stoltzfus
Date: Fri, 15 Jan 2021 11:45:24 -0500
Subject: [PATCH 14/38] [App Search] Add configurable Filter and Sort modal to
Documents View (#88066)
---
.../__mocks__/shallow_useeffect.mock.ts | 13 +-
.../customization_callout.test.tsx | 30 ++++
.../customization_callout.tsx | 48 ++++++
.../customization_modal.test.tsx | 75 +++++++++
.../search_experience/customization_modal.tsx | 156 ++++++++++++++++++
.../search_experience/search_experience.scss | 5 +
.../search_experience.test.tsx | 51 ++++++
.../search_experience/search_experience.tsx | 30 +++-
.../shared/use_local_storage/index.ts | 7 +
.../use_local_storage.test.tsx | 69 ++++++++
.../use_local_storage/use_local_storage.ts | 56 +++++++
11 files changed, 534 insertions(+), 6 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/use_local_storage.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/use_local_storage.ts
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts
index 1e3a45a83853c..ce4ec39505732 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_useeffect.mock.ts
@@ -4,15 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
+// Helper for calling the returned useEffect unmount handler
+let mockUnmountHandler: () => void;
+export const unmountHandler = () => mockUnmountHandler();
+
jest.mock('react', () => ({
...(jest.requireActual('react') as object),
- useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior
+ useEffect: jest.fn((fn) => {
+ mockUnmountHandler = fn();
+ return mockUnmountHandler;
+ }), // Calls on mount/every update - use mount for more complex behavior
}));
-// Helper for calling the returned useEffect unmount handler
-import { useEffect } from 'react';
-export const unmountHandler = () => (useEffect as jest.Mock).mock.calls[0][0]()();
-
/**
* Example usage within a component test using shallow():
*
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx
new file mode 100644
index 0000000000000..7864a6411ffa6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+import { EuiButton } from '@elastic/eui';
+
+import { CustomizationCallout } from './customization_callout';
+
+describe('CustomizationCallout', () => {
+ let wrapper: ShallowWrapper;
+ const onClick = jest.fn();
+
+ beforeAll(() => {
+ wrapper = shallow();
+ });
+
+ it('renders', () => {
+ expect(wrapper.isEmptyRender()).toBe(false);
+ });
+
+ it('calls onClick param when the Customize button is clicked', () => {
+ wrapper.find(EuiButton).simulate('click');
+ expect(onClick).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx
new file mode 100644
index 0000000000000..a9bec6ced828f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { EuiButton, EuiFlexGroup, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
+
+interface Props {
+ onClick(): void;
+}
+
+export const CustomizationCallout: React.FC = ({ onClick }) => {
+ return (
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.documents.search.customizationCallout.message',
+ {
+ defaultMessage:
+ 'Did you know that you can customize your document search experience? Click "Customize" below to get started.',
+ }
+ )}
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.documents.search.customizationCallout.button',
+ {
+ defaultMessage: 'Customize',
+ }
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx
new file mode 100644
index 0000000000000..94b2ab7cf3f04
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { setMockValues, setMockActions } from '../../../../__mocks__';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
+
+import { CustomizationModal } from './customization_modal';
+
+describe('CustomizationModal', () => {
+ const props = {
+ filterFields: ['field1', 'field2'],
+ sortFields: ['sortField1', 'sortField2'],
+ onClose: jest.fn(),
+ onSave: jest.fn(),
+ };
+
+ const values = {
+ engine: {
+ name: 'some-engine',
+ apiKey: '1234',
+ },
+ };
+
+ const actions = {
+ setEngine: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockValues(values);
+ setMockActions(actions);
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+ expect(wrapper.isEmptyRender()).toBe(false);
+ });
+
+ it('when save is clicked, it calls onSave prop with selected filter and sort fields', () => {
+ const wrapper = shallow();
+ wrapper.find(EuiButton).simulate('click');
+ expect(props.onSave).toHaveBeenCalledWith({
+ filterFields: ['field1', 'field2'],
+ sortFields: ['sortField1', 'sortField2'],
+ });
+ });
+
+ it('when save is clicked, it calls onSave prop when with updated selections', () => {
+ const wrapper = shallow();
+
+ const sortFieldsDropdown = wrapper.find('[data-test-subj="sortFieldsDropdown"]');
+ sortFieldsDropdown.simulate('change', [{ label: 'newSort1' }]);
+
+ const filterFieldsDropdown = wrapper.find('[data-test-subj="filterFieldsDropdown"]');
+ filterFieldsDropdown.simulate('change', [{ label: 'newField1' }]);
+
+ wrapper.find(EuiButton).simulate('click');
+ expect(props.onSave).toHaveBeenCalledWith({
+ filterFields: ['newField1'],
+ sortFields: ['newSort1'],
+ });
+ });
+
+ it('calls onClose when cancel is clicked', () => {
+ const wrapper = shallow();
+ wrapper.find(EuiButtonEmpty).simulate('click');
+ expect(props.onClose).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx
new file mode 100644
index 0000000000000..2b05ed7e78f64
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useMemo } from 'react';
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiComboBox,
+ EuiForm,
+ EuiFormRow,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiOverlayMask,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useValues } from 'kea';
+
+import { EngineLogic } from '../../engine';
+
+interface Props {
+ filterFields: string[];
+ sortFields: string[];
+ onClose(): void;
+ onSave({ sortFields, filterFields }: { sortFields: string[]; filterFields: string[] }): void;
+}
+
+const fieldNameToComboBoxOption = (fieldName: string) => ({ label: fieldName });
+const comboBoxOptionToFieldName = ({ label }: { label: string }) => label;
+
+export const CustomizationModal: React.FC = ({
+ filterFields,
+ onClose,
+ onSave,
+ sortFields,
+}) => {
+ const { engine } = useValues(EngineLogic);
+
+ const [selectedFilterFields, setSelectedFilterFields] = useState(
+ filterFields.map(fieldNameToComboBoxOption)
+ );
+ const [selectedSortFields, setSelectedSortFields] = useState(
+ sortFields.map(fieldNameToComboBoxOption)
+ );
+
+ const engineSchema = engine.schema || {};
+ const selectableFilterFields = useMemo(
+ () => Object.keys(engineSchema).map(fieldNameToComboBoxOption),
+ [engineSchema]
+ );
+ const selectableSortFields = useMemo(
+ () => Object.keys(engineSchema).map(fieldNameToComboBoxOption),
+ [engineSchema]
+ );
+
+ return (
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.title',
+ {
+ defaultMessage: 'Customize document search',
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.cancel',
+ {
+ defaultMessage: 'Cancel',
+ }
+ )}
+
+ {
+ onSave({
+ filterFields: selectedFilterFields.map(comboBoxOptionToFieldName),
+ sortFields: selectedSortFields.map(comboBoxOptionToFieldName),
+ });
+ }}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.save',
+ {
+ defaultMessage: 'Save',
+ }
+ )}
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss
index 868a561a27873..ba9931dc90fdc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss
@@ -20,4 +20,9 @@
.documentsSearchExperience__pagingInfo {
flex-grow: 0;
}
+
+ .customizationCallout {
+ background-color: $euiPageBackgroundColor;
+ padding: $euiSizeL;
+ }
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx
index 750d00311255c..250cd00943d7e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx
@@ -7,11 +7,26 @@ import '../../../../__mocks__/kea.mock';
import { setMockValues } from '../../../../__mocks__';
import '../../../../__mocks__/enterprise_search_url.mock';
+const mockSetFields = jest.fn();
+
+jest.mock('../../../../shared/use_local_storage', () => ({
+ useLocalStorage: jest.fn(() => [
+ {
+ filterFields: ['a', 'b', 'c'],
+ sortFields: ['d', 'c'],
+ },
+ mockSetFields,
+ ]),
+}));
+
import React from 'react';
// @ts-expect-error types are not available for this package yet
import { SearchProvider } from '@elastic/react-search-ui';
import { shallow } from 'enzyme';
+import { CustomizationCallout } from './customization_callout';
+import { CustomizationModal } from './customization_modal';
+
import { SearchExperience } from './search_experience';
describe('SearchExperience', () => {
@@ -31,4 +46,40 @@ describe('SearchExperience', () => {
const wrapper = shallow();
expect(wrapper.find(SearchProvider).length).toBe(1);
});
+
+ describe('customization modal', () => {
+ it('has a customization modal which can be opened and closed', () => {
+ const wrapper = shallow();
+ expect(wrapper.find(CustomizationModal).exists()).toBe(false);
+
+ wrapper.find(CustomizationCallout).simulate('click');
+ expect(wrapper.find(CustomizationModal).exists()).toBe(true);
+
+ wrapper.find(CustomizationModal).prop('onClose')();
+ expect(wrapper.find(CustomizationModal).exists()).toBe(false);
+ });
+
+ it('passes values from localStorage to the customization modal', () => {
+ const wrapper = shallow();
+ wrapper.find(CustomizationCallout).simulate('click');
+ expect(wrapper.find(CustomizationModal).prop('filterFields')).toEqual(['a', 'b', 'c']);
+ expect(wrapper.find(CustomizationModal).prop('sortFields')).toEqual(['d', 'c']);
+ });
+
+ it('updates selected fields in localStorage and closes modal on save', () => {
+ const wrapper = shallow();
+ wrapper.find(CustomizationCallout).simulate('click');
+ wrapper.find(CustomizationModal).prop('onSave')({
+ filterFields: ['new', 'filters'],
+ sortFields: ['new', 'sorts'],
+ });
+
+ expect(mockSetFields).toHaveBeenCalledWith({
+ filterFields: ['new', 'filters'],
+ sortFields: ['new', 'sorts'],
+ });
+
+ expect(wrapper.find(CustomizationModal).exists()).toBe(false);
+ });
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx
index 1501efc589fc0..e80ab2e18b2d3 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useValues } from 'kea';
@@ -17,10 +17,13 @@ import './search_experience.scss';
import { EngineLogic } from '../../engine';
import { externalUrl } from '../../../../shared/enterprise_search_url';
+import { useLocalStorage } from '../../../../shared/use_local_storage';
import { SearchBoxView, SortingView } from './views';
import { SearchExperienceContent } from './search_experience_content';
import { buildSearchUIConfig } from './build_search_ui_config';
+import { CustomizationCallout } from './customization_callout';
+import { CustomizationModal } from './customization_modal';
const DEFAULT_SORT_OPTIONS = [
{
@@ -43,6 +46,18 @@ export const SearchExperience: React.FC = () => {
const { engine } = useValues(EngineLogic);
const endpointBase = externalUrl.enterpriseSearchUrl;
+ const [showCustomizationModal, setShowCustomizationModal] = useState(false);
+ const openCustomizationModal = () => setShowCustomizationModal(true);
+ const closeCustomizationModal = () => setShowCustomizationModal(false);
+
+ const [fields, setFields] = useLocalStorage(
+ `documents-search-experience-customization--${engine.name}`,
+ {
+ filterFields: [] as string[],
+ sortFields: [] as string[],
+ }
+ );
+
// TODO const sortFieldsOptions = _flatten(fields.sortFields.map(fieldNameToSortOptions)) // we need to flatten this array since fieldNameToSortOptions returns an array of two sorting options
const sortingOptions = [...DEFAULT_SORT_OPTIONS /* TODO ...sortFieldsOptions*/];
@@ -85,12 +100,25 @@ export const SearchExperience: React.FC = () => {
sortOptions={sortingOptions}
view={SortingView}
/>
+
+
+ {showCustomizationModal && (
+ {
+ setFields({ filterFields, sortFields });
+ closeCustomizationModal();
+ }}
+ />
+ )}
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/index.ts
new file mode 100644
index 0000000000000..8c75ca9ae43c8
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { useLocalStorage } from './use_local_storage';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/use_local_storage.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/use_local_storage.test.tsx
new file mode 100644
index 0000000000000..0b0edcdf86f6a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/use_local_storage/use_local_storage.test.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { useLocalStorage } from './use_local_storage';
+
+describe('useLocalStorage', () => {
+ const KEY = 'fields';
+
+ const TestComponent = () => {
+ const [fields, setFields] = useLocalStorage(KEY, {
+ options: ['foo', 'bar', 'baz'],
+ });
+ return (
+