From 23666832091d0a30c00222fdd73d56af51224ff9 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 23 Jun 2021 14:43:17 -0500 Subject: [PATCH 01/86] [Enterprise Search] Add shared Users components and enable RBAC functionality (#102826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add RolesEmptyPrompt component * Move constants to shared Will be used in next commit so DRYing them out here * Add UserAddedInfo component * Add UsersEmptyPrompt component * Add UserInvitationCallout component * Add some shared types * Add UserSelector component * Fix imports from a previous commit Refactored these to shared but missed updating the implementation. See e2d3ec2ca4aba3cb6f7e8e2d2d2da96aa6bedf1b * Add UsersHeading component * Add UserFlyout component * Update UsersAndRolesRowActions with confirm modal Design calls for using a custom call out instead of window.confirm * Add pagination size and fix type - email can be null on bult-in elasticsearch users * Add UsersTable component * Remove window.confirm from logic files The UsersAndRolesRowActions component now uses an EUI prompt for this. Whitespace changes should be hidden for this commit * Add routes for enabling RBAC * Update App Search routes https://github.com/elastic/ent-search/pull/3862 added the ‘/as’ prefix to App Search role mappings routes * Add logic for enabling role-based access * Pass docsLink as a prop to the heading component * Add empty states to mappings landing pages * Fix a couple of missed i18ns * Remove unused translations * Remove EuiOverlayMask This was needed in ent-search because it uses an older EUI. The newer confirm modal has its own overlay * Update RoleMappingsTable to use new design Previously, we showed all engines/groups in the table but the new design calls for a truncated list with additional items so [‘foo’, ‘bar’, ‘baz’] would display as “foo, bar + 1” This is already in place for the users table * Lint fix * Another lint fix * Fix test name Co-authored-by: Jason Stoltzfus * Move test Co-authored-by: Jason Stoltzfus --- .../components/role_mappings/constants.ts | 8 - .../role_mappings/role_mappings.tsx | 22 +- .../role_mappings/role_mappings_logic.test.ts | 49 ++-- .../role_mappings/role_mappings_logic.ts | 37 ++- .../applications/shared/constants/index.ts | 1 + .../applications/shared/constants/labels.ts | 15 ++ .../__mocks__/elasticsearch_users.ts | 13 ++ .../shared/role_mapping/__mocks__/roles.ts | 19 ++ .../shared/role_mapping/constants.ts | 213 +++++++++++++++++- .../applications/shared/role_mapping/index.ts | 8 + .../role_mappings_heading.test.tsx | 8 +- .../role_mapping/role_mappings_heading.tsx | 8 +- .../role_mapping/role_mappings_table.test.tsx | 34 +-- .../role_mapping/role_mappings_table.tsx | 37 ++- .../role_mapping/roles_empty_prompt.test.tsx | 39 ++++ .../role_mapping/roles_empty_prompt.tsx | 48 ++++ .../role_mapping/user_added_info.test.tsx | 28 +++ .../shared/role_mapping/user_added_info.tsx | 40 ++++ .../shared/role_mapping/user_flyout.test.tsx | 70 ++++++ .../shared/role_mapping/user_flyout.tsx | 113 ++++++++++ .../user_invitation_callout.test.tsx | 46 ++++ .../role_mapping/user_invitation_callout.tsx | 47 ++++ .../role_mapping/user_selector.test.tsx | 112 +++++++++ .../shared/role_mapping/user_selector.tsx | 159 +++++++++++++ .../users_and_roles_row_actions.test.tsx | 22 +- .../users_and_roles_row_actions.tsx | 63 +++++- .../role_mapping/users_empty_prompt.test.tsx | 22 ++ .../role_mapping/users_empty_prompt.tsx | 43 ++++ .../role_mapping/users_heading.test.tsx | 32 +++ .../shared/role_mapping/users_heading.tsx | 37 +++ .../shared/role_mapping/users_table.test.tsx | 100 ++++++++ .../shared/role_mapping/users_table.tsx | 147 ++++++++++++ .../public/applications/shared/types.ts | 16 ++ .../groups/components/group_users_table.tsx | 18 +- .../views/role_mappings/constants.ts | 8 - .../views/role_mappings/role_mappings.tsx | 27 ++- .../role_mappings/role_mappings_logic.test.ts | 50 ++-- .../role_mappings/role_mappings_logic.ts | 37 ++- .../routes/app_search/role_mappings.test.ts | 37 ++- .../server/routes/app_search/role_mappings.ts | 24 +- .../workplace_search/role_mappings.test.ts | 29 ++- .../routes/workplace_search/role_mappings.ts | 16 ++ .../translations/translations/ja-JP.json | 9 +- .../translations/translations/zh-CN.json | 9 +- 44 files changed, 1748 insertions(+), 172 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index df1e19e264c75..cce18cbeffd0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -9,14 +9,6 @@ import { i18n } from '@kbn/i18n'; import { AdvanceRoleType } from '../../types'; -export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage', - { - defaultMessage: - 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', - } -); - export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index db0e6e6dead11..03e2ae67eca9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -10,16 +10,25 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; +import { + RoleMappingsTable, + RoleMappingsHeading, + RolesEmptyPrompt, +} from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; + +import { DOCS_PREFIX } from '../../routes'; import { AppSearchPageTemplate } from '../layout'; import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants'; import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; +const ROLES_DOCS_LINK = `${DOCS_PREFIX}/security-and-users.html`; + export const RoleMappings: React.FC = () => { const { + enableRoleBasedAccess, initializeRoleMappings, initializeRoleMapping, handleDeleteMapping, @@ -37,10 +46,19 @@ export const RoleMappings: React.FC = () => { return resetState; }, []); + const rolesEmptyState = ( + + ); + const roleMappingsSection = (
initializeRoleMapping()} /> { pageChrome={[ROLE_MAPPINGS_TITLE]} pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }} isLoading={dataLoading} + isEmptyState={roleMappings.length < 1} + emptyState={rolesEmptyState} > {roleMappingFlyoutOpen && } {roleMappingsSection} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index 870e303a2930d..6985f213d1dd5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -87,6 +87,13 @@ describe('RoleMappingsLogic', () => { }); }); + it('setRoleMappings', () => { + RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [asRoleMapping] }); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + }); + it('handleRoleChange', () => { RoleMappingsLogic.actions.handleRoleChange('dev'); @@ -266,6 +273,30 @@ describe('RoleMappingsLogic', () => { }); describe('listeners', () => { + describe('enableRoleBasedAccess', () => { + it('calls API and sets values', async () => { + const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings'); + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + + expect(RoleMappingsLogic.values.dataLoading).toEqual(true); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/role_mappings/enable_role_based_access' + ); + await nextTick(); + expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + describe('initializeRoleMappings', () => { it('calls API and sets values', async () => { const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); @@ -400,18 +431,8 @@ describe('RoleMappingsLogic', () => { }); describe('handleDeleteMapping', () => { - let confirmSpy: any; const roleMappingId = 'r1'; - beforeEach(() => { - confirmSpy = jest.spyOn(window, 'confirm'); - confirmSpy.mockImplementation(jest.fn(() => true)); - }); - - afterEach(() => { - confirmSpy.mockRestore(); - }); - it('calls API and refreshes list', async () => { mount(mappingsServerProps); const initializeRoleMappingsSpy = jest.spyOn( @@ -436,14 +457,6 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); - - it('will do nothing if not confirmed', () => { - mount(mappingsServerProps); - jest.spyOn(window, 'confirm').mockReturnValueOnce(false); - RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); - - expect(http.delete).not.toHaveBeenCalled(); - }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index fc0a235b23c77..e2ef75897528c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -22,7 +22,6 @@ import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; import { Engine } from '../engine/types'; import { - DELETE_ROLE_MAPPING_MESSAGE, ROLE_MAPPING_DELETED_MESSAGE, ROLE_MAPPING_CREATED_MESSAGE, ROLE_MAPPING_UPDATED_MESSAGE, @@ -59,10 +58,16 @@ interface RoleMappingsActions { initializeRoleMappings(): void; resetState(): void; setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping }; + setRoleMappings({ + roleMappings, + }: { + roleMappings: ASRoleMapping[]; + }): { roleMappings: ASRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; openRoleMappingFlyout(): void; closeRoleMappingFlyout(): void; setRoleMappingErrors(errors: string[]): { errors: string[] }; + enableRoleBasedAccess(): void; } interface RoleMappingsValues { @@ -91,6 +96,7 @@ export const RoleMappingsLogic = kea data, setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }), + setRoleMappings: ({ roleMappings }: { roleMappings: ASRoleMapping[] }) => ({ roleMappings }), setRoleMappingErrors: (errors: string[]) => ({ errors }), handleAuthProviderChange: (value: string) => ({ value }), handleRoleChange: (roleType: RoleTypes) => ({ roleType }), @@ -101,6 +107,7 @@ export const RoleMappingsLogic = kea ({ value }), handleAccessAllEnginesChange: (selected: boolean) => ({ selected }), + enableRoleBasedAccess: true, resetState: true, initializeRoleMappings: true, initializeRoleMapping: (roleMappingId) => ({ roleMappingId }), @@ -114,13 +121,16 @@ export const RoleMappingsLogic = kea false, + setRoleMappings: () => false, resetState: () => true, + enableRoleBasedAccess: () => true, }, ], roleMappings: [ [], { setRoleMappingsData: (_, { roleMappings }) => roleMappings, + setRoleMappings: (_, { roleMappings }) => roleMappings, resetState: () => [], }, ], @@ -267,6 +277,17 @@ export const RoleMappingsLogic = kea ({ + enableRoleBasedAccess: async () => { + const { http } = HttpLogic.values; + const route = '/api/app_search/role_mappings/enable_role_based_access'; + + try { + const response = await http.post(route); + actions.setRoleMappings(response); + } catch (e) { + flashAPIErrors(e); + } + }, initializeRoleMappings: async () => { const { http } = HttpLogic.values; const route = '/api/app_search/role_mappings'; @@ -286,14 +307,12 @@ export const RoleMappingsLogic = kea { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts index 70990727b8a62..b15bd9e1155cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts @@ -6,4 +6,5 @@ */ export * from './actions'; +export * from './labels'; export { DEFAULT_META } from './default_meta'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts new file mode 100644 index 0000000000000..8e6159d2b5b2a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.usernameLabel', { + defaultMessage: 'Username', +}); +export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.emailLabel', { + defaultMessage: 'Email', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts new file mode 100644 index 0000000000000..500f560675679 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const elasticsearchUsers = [ + { + email: 'user1@user.com', + username: 'user1', + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts index 15dec753351ba..486c1ba6c9af6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts @@ -9,6 +9,8 @@ import { engines } from '../../../app_search/__mocks__/engines.mock'; import { AttributeName } from '../../types'; +import { elasticsearchUsers } from './elasticsearch_users'; + export const asRoleMapping = { id: 'sdgfasdgadf123', attributeName: 'role' as AttributeName, @@ -70,3 +72,20 @@ export const wsRoleMapping = { }, ], }; + +export const invitation = { + email: 'foo@example.com', + code: '123fooqwe', +}; + +export const wsSingleUserRoleMapping = { + invitation, + elasticsearchUser: elasticsearchUsers[0], + roleMapping: wsRoleMapping, +}; + +export const asSingleUserRoleMapping = { + invitation, + elasticsearchUser: elasticsearchUsers[0], + roleMapping: asRoleMapping, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 9f40844e52470..45cab32b67e08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -50,10 +50,26 @@ export const ROLE_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.rol defaultMessage: 'Role', }); +export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.usernameLabel', { + defaultMessage: 'Username', +}); + +export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.emailLabel', { + defaultMessage: 'Email', +}); + export const ALL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.allLabel', { defaultMessage: 'All', }); +export const GROUPS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.groupsLabel', { + defaultMessage: 'Groups', +}); + +export const ENGINES_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.enginesLabel', { + defaultMessage: 'Engines', +}); + export const AUTH_PROVIDER_LABEL = i18n.translate( 'xpack.enterpriseSearch.roleMapping.authProviderLabel', { @@ -82,10 +98,10 @@ export const ATTRIBUTE_VALUE_ERROR = i18n.translate( } ); -export const DELETE_ROLE_MAPPING_TITLE = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle', +export const REMOVE_ROLE_MAPPING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle', { - defaultMessage: 'Remove this role mapping', + defaultMessage: 'Remove role mapping', } ); @@ -96,10 +112,17 @@ export const DELETE_ROLE_MAPPING_DESCRIPTION = i18n.translate( } ); -export const DELETE_ROLE_MAPPING_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton', +export const REMOVE_ROLE_MAPPING_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.removeRoleMappingButton', + { + defaultMessage: 'Remove mapping', + } +); + +export const REMOVE_USER_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.removeUserButton', { - defaultMessage: 'Delete mapping', + defaultMessage: 'Remove user', } ); @@ -205,3 +228,181 @@ export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.roleMapping.noResults.message', { defaultMessage: 'Create a new role mapping' } ); + +export const ROLES_DISABLED_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.rolesDisabledTitle', + { defaultMessage: 'Role-based access is disabled' } +); + +export const ROLES_DISABLED_DESCRIPTION = (productName: ProductName) => + i18n.translate('xpack.enterpriseSearch.roleMapping.rolesDisabledDescription', { + defaultMessage: + 'All users set for this deployment currently have full access to {productName}. To restrict access and manage permissions, you must enable role-based access for Enterprise Search.', + values: { productName }, + }); + +export const ROLES_DISABLED_NOTE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.rolesDisabledNote', + { + defaultMessage: + 'Note: enabling role-based access restricts access for both App Search and Workplace Search. Once enabled, review access management for both products, if applicable.', + } +); + +export const ENABLE_ROLES_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.enableRolesButton', + { defaultMessage: 'Enable role-based access' } +); + +export const ENABLE_ROLES_LINK = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.enableRolesLink', + { defaultMessage: 'Learn more about role-based access' } +); + +export const INVITATION_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.invitationDescription', + { + defaultMessage: + 'This URL can be shared with the user, allowing them to accept the Enterprise Search invitation and set a new password', + } +); + +export const NEW_INVITATION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.newInvitationLabel', + { defaultMessage: 'Invitation URL' } +); + +export const EXISTING_INVITATION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.existingInvitationLabel', + { defaultMessage: 'The user has not yet accepted the invitation.' } +); + +export const INVITATION_LINK = i18n.translate('xpack.enterpriseSearch.roleMapping.invitationLink', { + defaultMessage: 'Enterprise Search Invitation Link', +}); + +export const NO_USERS_TITLE = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersTitle', { + defaultMessage: 'No user added', +}); + +export const NO_USERS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.noUsersDescription', + { + defaultMessage: + 'Users can be added individually, for flexibility. Role mappings provide a broader interface for adding large number of users using user attributes.', + } +); + +export const ENABLE_USERS_LINK = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.enableUsersLink', + { defaultMessage: 'Learn more about user management' } +); + +export const NEW_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.newUserLabel', { + defaultMessage: 'Create new user', +}); + +export const EXISTING_USER_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.existingUserLabel', + { defaultMessage: 'Add existing user' } +); + +export const USERNAME_NO_USERS_TEXT = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usernameNoUsersText', + { defaultMessage: 'No existing user eligible for addition.' } +); + +export const REQUIRED_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.requiredLabel', { + defaultMessage: 'Required', +}); + +export const USERS_HEADING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usersHeadingTitle', + { defaultMessage: 'Users' } +); + +export const USERS_HEADING_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usersHeadingDescription', + { + defaultMessage: + 'User management provides granular access for individual or special permission needs. Users from federated sources such as SAML are managed by role mappings, and excluded from this list.', + } +); + +export const USERS_HEADING_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usersHeadingLabel', + { defaultMessage: 'Add a new user' } +); + +export const UPDATE_USER_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.updateUserLabel', + { + defaultMessage: 'Update user', + } +); + +export const ADD_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.addUserLabel', { + defaultMessage: 'Add user', +}); + +export const USER_ADDED_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.userAddedLabel', + { + defaultMessage: 'User added', + } +); + +export const USER_UPDATED_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.userUpdatedLabel', + { + defaultMessage: 'User updated', + } +); + +export const NEW_USER_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.newUserDescription', + { + defaultMessage: 'Provide granular access and permissions', + } +); + +export const UPDATE_USER_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.updateUserDescription', + { + defaultMessage: 'Manage granular access and permissions', + } +); + +export const INVITATION_PENDING_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.invitationPendingLabel', + { + defaultMessage: 'Invitation pending', + } +); + +export const ROLE_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.roleModalText', { + defaultMessage: + 'Removing a role mapping revokes access to any user corresponding to the mapping attributes, but may not take effect immediately for SAML-governed roles. Users with an active SAML session will retain access until it expires.', +}); + +export const USER_MODAL_TITLE = (username: string) => + i18n.translate('xpack.enterpriseSearch.roleMapping.userModalTitle', { + defaultMessage: 'Remove {username}', + values: { username }, + }); + +export const USER_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.userModalText', { + defaultMessage: + 'Removing a user immediately revokes access to the experience, unless this user’s attributes also corresponds to a role mapping for native and SAML-governed authentication, in which case associated role mappings should also be reviewed and adjusted, as needed.', +}); + +export const FILTER_USERS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.filterUsersLabel', + { + defaultMessage: 'Filter users', + } +); + +export const NO_USERS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersLabel', { + defaultMessage: 'No matching users found', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index b0d10e9692714..8096b86939ff3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -6,9 +6,17 @@ */ export { AttributeSelector } from './attribute_selector'; +export { RolesEmptyPrompt } from './roles_empty_prompt'; export { RoleMappingsTable } from './role_mappings_table'; export { RoleOptionLabel } from './role_option_label'; export { RoleSelector } from './role_selector'; export { RoleMappingFlyout } from './role_mapping_flyout'; export { RoleMappingsHeading } from './role_mappings_heading'; +export { UserAddedInfo } from './user_added_info'; +export { UserFlyout } from './user_flyout'; +export { UsersHeading } from './users_heading'; +export { UserInvitationCallout } from './user_invitation_callout'; +export { UserSelector } from './user_selector'; +export { UsersTable } from './users_table'; export { UsersAndRolesRowActions } from './users_and_roles_row_actions'; +export { UsersEmptyPrompt } from './users_empty_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx index f0bf86fb306c6..5a2958d60dc2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx @@ -15,7 +15,13 @@ import { RoleMappingsHeading } from './role_mappings_heading'; describe('RoleMappingsHeading', () => { it('renders ', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper.find(EuiTitle)).toHaveLength(1); expect(wrapper.find(EuiText)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx index eee8b180d3281..1984cc6c60a34 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx @@ -28,13 +28,11 @@ import { interface Props { productName: ProductName; + docsLink: string; onClick(): void; } -// TODO: Replace EuiLink href with acutal docs link when available -const ROLE_MAPPINGS_DOCS_HREF = '#TODO'; - -export const RoleMappingsHeading: React.FC = ({ productName, onClick }) => ( +export const RoleMappingsHeading: React.FC = ({ productName, docsLink, onClick }) => (
@@ -45,7 +43,7 @@ export const RoleMappingsHeading: React.FC = ({ productName, onClick }) =

{ROLE_MAPPINGS_HEADING_DESCRIPTION(productName)}{' '} - + {ROLE_MAPPINGS_HEADING_DOCS_LINK}

diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 156b52a4016c3..81a7c06020165 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -13,7 +13,9 @@ import { mount } from 'enzyme'; import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui'; -import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; +import { engines } from '../../app_search/__mocks__/engines.mock'; + +import { ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; import { RoleMappingsTable } from './role_mappings_table'; import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; @@ -78,28 +80,30 @@ describe('RoleMappingsTable', () => { expect(handleDeleteMapping).toHaveBeenCalled(); }); - it('shows default message when "accessAllEngines" is true', () => { + it('handles access items display for all items', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="AccessItemsList"]').prop('children')).toEqual(ALL_LABEL); + expect(wrapper.find('[data-test-subj="AllItems"]')).toHaveLength(1); }); - it('handles display when no items present', () => { - const noItemsRoleMapping = { ...asRoleMapping, engines: [] }; - noItemsRoleMapping.accessAllEngines = false; - + it('handles access items display more than 2 items', () => { + const extraEngine = { + ...engines[0], + id: '3', + }; + + const roleMapping = { + ...asRoleMapping, + engines: [...engines, extraEngine], + accessAllEngines: false, + }; const wrapper = mount( - + ); - - expect(wrapper.find('[data-test-subj="AccessItemsList"]').children().children().text()).toEqual( - '—' + expect(wrapper.find('[data-test-subj="AccessItems"]').prop('children')).toEqual( + `${engines[0].name}, ${engines[1].name} + 1` ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index 7696cf03ed4b1..eb9621c7a242c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; -import { EuiIconTip, EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { WSRoleMapping } from '../../workplace_search/types'; @@ -46,8 +46,6 @@ interface Props { handleDeleteMapping(roleMappingId: string): void; } -const noItemsPlaceholder = ; - const getAuthProviderDisplayValue = (authProvider: string) => authProvider === ANY_AUTH_PROVIDER ? ANY_AUTH_PROVIDER_OPTION_LABEL : authProvider; @@ -90,24 +88,18 @@ export const RoleMappingsTable: React.FC = ({ const accessItemsCol: EuiBasicTableColumn = { field: 'accessItems', name: accessHeader, - render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => ( - - {accessAllEngines ? ( - ALL_LABEL - ) : ( - <> - {accessItems.length === 0 - ? noItemsPlaceholder - : accessItems.map(({ name }) => ( - - {name} -
-
- ))} - - )} -
- ), + render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => { + // Design calls for showing the first 2 items followed by a +x after those 2. + // ['foo', 'bar', 'baz'] would display as: "foo, bar + 1" + const numItems = accessItems.length; + if (accessAllEngines || numItems === 0) + return {ALL_LABEL}; + const additionalItems = numItems > 2 ? ` + ${numItems - 2}` : ''; + const names = accessItems.map((item) => item.name); + return ( + {names.slice(0, 2).join(', ') + additionalItems} + ); + }, }; const authProviderCol: EuiBasicTableColumn = { @@ -143,6 +135,7 @@ export const RoleMappingsTable: React.FC = ({ const pagination = { hidePerPageOptions: true, + pageSize: 10, }; const search = { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx new file mode 100644 index 0000000000000..8331a45849e3a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiLink, EuiEmptyPrompt } from '@elastic/eui'; + +import { RolesEmptyPrompt } from './roles_empty_prompt'; + +describe('RolesEmptyPrompt', () => { + const onEnable = jest.fn(); + + const props = { + productName: 'App Search', + docsLink: 'http://elastic.co', + onEnable, + }; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(EuiEmptyPrompt).dive().find(EuiLink).prop('href')).toEqual(props.docsLink); + }); + + it('calls onEnable on change', () => { + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive(); + prompt.find(EuiButton).simulate('click'); + + expect(onEnable).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx new file mode 100644 index 0000000000000..11d50573c45f6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiEmptyPrompt, EuiButton, EuiLink, EuiSpacer } from '@elastic/eui'; + +import { ProductName } from '../types'; + +import { + ROLES_DISABLED_TITLE, + ROLES_DISABLED_DESCRIPTION, + ROLES_DISABLED_NOTE, + ENABLE_ROLES_BUTTON, + ENABLE_ROLES_LINK, +} from './constants'; + +interface Props { + productName: ProductName; + docsLink: string; + onEnable(): void; +} + +export const RolesEmptyPrompt: React.FC = ({ onEnable, docsLink, productName }) => ( + {ROLES_DISABLED_TITLE}} + body={ + <> +

{ROLES_DISABLED_DESCRIPTION(productName)}

+

{ROLES_DISABLED_NOTE}

+ + } + actions={[ + + {ENABLE_ROLES_BUTTON} + , + , + + {ENABLE_ROLES_LINK} + , + ]} + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx new file mode 100644 index 0000000000000..30bdaa0010b58 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText } from '@elastic/eui'; + +import { UserAddedInfo } from './'; + +describe('UserAddedInfo', () => { + const props = { + username: 'user1', + email: 'test@test.com', + roleType: 'user', + }; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiText)).toHaveLength(6); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx new file mode 100644 index 0000000000000..a12eae66262a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; + +import { ROLE_LABEL } from './constants'; + +interface Props { + username: string; + email: string; + roleType: string; +} + +export const UserAddedInfo: React.FC = ({ username, email, roleType }) => ( + <> + + {USERNAME_LABEL} + + {username} + + + {EMAIL_LABEL} + + {email} + + + {ROLE_LABEL} + + {roleType} + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx new file mode 100644 index 0000000000000..43333fe048f23 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout, EuiText, EuiIcon } from '@elastic/eui'; + +import { + USERS_HEADING_LABEL, + UPDATE_USER_LABEL, + USER_UPDATED_LABEL, + NEW_USER_DESCRIPTION, + UPDATE_USER_DESCRIPTION, +} from './constants'; + +import { UserFlyout } from './'; + +describe('UserFlyout', () => { + const closeUserFlyout = jest.fn(); + const handleSaveUser = jest.fn(); + + const props = { + children:
, + isNew: true, + isComplete: false, + disabled: false, + closeUserFlyout, + handleSaveUser, + }; + + it('renders for new user', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFlyout)).toHaveLength(1); + expect(wrapper.find('h2').prop('children')).toEqual(USERS_HEADING_LABEL); + expect(wrapper.find(EuiText).prop('children')).toEqual(

{NEW_USER_DESCRIPTION}

); + }); + + it('renders for existing user', () => { + const wrapper = shallow(); + + expect(wrapper.find('h2').prop('children')).toEqual(UPDATE_USER_LABEL); + expect(wrapper.find(EuiText).prop('children')).toEqual(

{UPDATE_USER_DESCRIPTION}

); + }); + + it('renders icon and message for completed user', () => { + const wrapper = shallow(); + const icon = ( + + ); + const children = ( + + {USER_UPDATED_LABEL} {icon} + + ); + + expect(wrapper.find('h2').prop('children')).toEqual(children); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx new file mode 100644 index 0000000000000..e13a56a716929 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiIcon, + EuiText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +interface Props { + children: React.ReactNode; + isNew: boolean; + isComplete: boolean; + disabled: boolean; + closeUserFlyout(): void; + handleSaveUser(): void; +} + +import { CANCEL_BUTTON_LABEL, CLOSE_BUTTON_LABEL } from '../constants'; + +import { + USERS_HEADING_LABEL, + UPDATE_USER_LABEL, + ADD_USER_LABEL, + USER_ADDED_LABEL, + USER_UPDATED_LABEL, + NEW_USER_DESCRIPTION, + UPDATE_USER_DESCRIPTION, +} from './constants'; + +export const UserFlyout: React.FC = ({ + children, + isNew, + isComplete, + disabled, + closeUserFlyout, + handleSaveUser, +}) => { + const savedIcon = ( + + ); + const IS_EDITING_HEADING = isNew ? USERS_HEADING_LABEL : UPDATE_USER_LABEL; + const IS_EDITING_DESCRIPTION = isNew ? NEW_USER_DESCRIPTION : UPDATE_USER_DESCRIPTION; + const USER_SAVED_HEADING = isNew ? USER_ADDED_LABEL : USER_UPDATED_LABEL; + const IS_COMPLETE_HEADING = ( + + {USER_SAVED_HEADING} {savedIcon} + + ); + + const editingFooterActions = ( + + + {CANCEL_BUTTON_LABEL} + + + + {isNew ? ADD_USER_LABEL : UPDATE_USER_LABEL} + + + + ); + + const completedFooterAction = ( + + + + {CLOSE_BUTTON_LABEL} + + + + ); + + return ( + + + +

{isComplete ? IS_COMPLETE_HEADING : IS_EDITING_HEADING}

+
+ {!isComplete && ( + +

{IS_EDITING_DESCRIPTION}

+
+ )} +
+ + {children} + + + {isComplete ? completedFooterAction : editingFooterActions} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx new file mode 100644 index 0000000000000..d5272a26715b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText, EuiButtonIcon, EuiCopy } from '@elastic/eui'; + +import { EXISTING_INVITATION_LABEL } from './constants'; + +import { UserInvitationCallout } from './'; + +describe('UserInvitationCallout', () => { + const props = { + isNew: true, + invitationCode: 'test@test.com', + urlPrefix: 'http://foo', + }; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiText)).toHaveLength(2); + }); + + it('renders the copy button', () => { + const copyMock = jest.fn(); + const wrapper = shallow(); + + const copyEl = shallow(
{wrapper.find(EuiCopy).props().children(copyMock)}
); + expect(copyEl.find(EuiButtonIcon).props().onClick).toEqual(copyMock); + }); + + it('renders existing invitation label', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiText).first().prop('children')).toEqual( + {EXISTING_INVITATION_LABEL} + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx new file mode 100644 index 0000000000000..8310077ad6f2e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiCopy, EuiButtonIcon, EuiSpacer, EuiText, EuiLink } from '@elastic/eui'; + +import { + INVITATION_DESCRIPTION, + NEW_INVITATION_LABEL, + EXISTING_INVITATION_LABEL, + INVITATION_LINK, +} from './constants'; + +interface Props { + isNew: boolean; + invitationCode: string; + urlPrefix: string; +} + +export const UserInvitationCallout: React.FC = ({ isNew, invitationCode, urlPrefix }) => { + const link = urlPrefix + invitationCode; + const label = isNew ? NEW_INVITATION_LABEL : EXISTING_INVITATION_LABEL; + + return ( + <> + {!isNew && } + + {label} + + + {INVITATION_DESCRIPTION} + + + {INVITATION_LINK} + {' '} + + {(copy) => } + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx new file mode 100644 index 0000000000000..08ddc7ba5427f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchUsers } from './__mocks__/elasticsearch_users'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFormRow } from '@elastic/eui'; + +import { Role as ASRole } from '../../app_search/types'; + +import { REQUIRED_LABEL, USERNAME_NO_USERS_TEXT } from './constants'; + +import { UserSelector } from './'; + +const simulatedEvent = { + target: { value: 'foo' }, +}; + +describe('UserSelector', () => { + const setUserExisting = jest.fn(); + const setElasticsearchUsernameValue = jest.fn(); + const setElasticsearchEmailValue = jest.fn(); + const handleRoleChange = jest.fn(); + const handleUsernameSelectChange = jest.fn(); + + const roleType = ('user' as unknown) as ASRole; + + const props = { + isNewUser: true, + userFormUserIsExisting: true, + elasticsearchUsers, + elasticsearchUser: elasticsearchUsers[0], + roleTypes: [roleType], + roleType, + setUserExisting, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + }; + + it('renders Role select and calls method', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="RoleSelect"]').simulate('change', simulatedEvent); + + expect(handleRoleChange).toHaveBeenCalled(); + }); + + it('renders when updating user', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="UsernameInput"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="EmailInput"]')).toHaveLength(1); + }); + + it('renders Username select and calls method', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="UsernameSelect"]').simulate('change', simulatedEvent); + + expect(handleUsernameSelectChange).toHaveBeenCalled(); + }); + + it('renders Existing user radio and calls method', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="ExistingUserRadio"]').simulate('change'); + + expect(setUserExisting).toHaveBeenCalledWith(true); + }); + + it('renders Email input and calls method', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="EmailInput"]').simulate('change', simulatedEvent); + + expect(setElasticsearchEmailValue).toHaveBeenCalled(); + }); + + it('renders Username input and calls method', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="UsernameInput"]').simulate('change', simulatedEvent); + + expect(setElasticsearchUsernameValue).toHaveBeenCalled(); + }); + + it('renders New user radio and calls method', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="NewUserRadio"]').simulate('change'); + + expect(setUserExisting).toHaveBeenCalledWith(false); + }); + + it('renders helpText when values are empty', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiFormRow).at(0).prop('helpText')).toEqual(USERNAME_NO_USERS_TEXT); + expect(wrapper.find(EuiFormRow).at(1).prop('helpText')).toEqual(REQUIRED_LABEL); + expect(wrapper.find(EuiFormRow).at(2).prop('helpText')).toEqual(REQUIRED_LABEL); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx new file mode 100644 index 0000000000000..70348bf29894a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiFieldText, + EuiRadio, + EuiFormRow, + EuiSelect, + EuiSelectOption, + EuiSpacer, +} from '@elastic/eui'; + +import { Role as ASRole } from '../../app_search/types'; +import { ElasticsearchUser } from '../../shared/types'; +import { Role as WSRole } from '../../workplace_search/types'; + +import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; + +import { + NEW_USER_LABEL, + EXISTING_USER_LABEL, + USERNAME_NO_USERS_TEXT, + REQUIRED_LABEL, + ROLE_LABEL, +} from './constants'; + +type SharedRole = WSRole | ASRole; + +interface Props { + isNewUser: boolean; + userFormUserIsExisting: boolean; + elasticsearchUsers: ElasticsearchUser[]; + elasticsearchUser: ElasticsearchUser; + roleTypes: SharedRole[]; + roleType: SharedRole; + setUserExisting(userFormUserIsExisting: boolean): void; + setElasticsearchUsernameValue(username: string): void; + setElasticsearchEmailValue(email: string): void; + handleRoleChange(roleType: SharedRole): void; + handleUsernameSelectChange(username: string): void; +} + +export const UserSelector: React.FC = ({ + isNewUser, + userFormUserIsExisting, + elasticsearchUsers, + elasticsearchUser, + roleTypes, + roleType, + setUserExisting, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, +}) => { + const roleOptions = roleTypes.map((role) => ({ id: role, text: role })); + const usernameOptions = elasticsearchUsers.map(({ username }) => ({ + id: username, + text: username, + })); + const hasElasticsearchUsers = elasticsearchUsers.length > 0; + const showNewUserExistingUserControls = userFormUserIsExisting && hasElasticsearchUsers; + + const roleSelect = ( + + handleRoleChange(e.target.value as SharedRole)} + /> + + ); + + const emailInput = ( + + setElasticsearchEmailValue(e.target.value)} + /> + + ); + + const usernameAndEmailControls = ( + <> + + setElasticsearchUsernameValue(e.target.value)} + /> + + {elasticsearchUser.email !== null && emailInput} + {roleSelect} + + ); + + const existingUserControls = ( + <> + + + handleUsernameSelectChange(e.target.value)} + /> + + {roleSelect} + + ); + + const newUserControls = ( + <> + + {usernameAndEmailControls} + + ); + + const createUserControls = ( + <> + + setUserExisting(true)} + disabled={!hasElasticsearchUsers} + /> + + + {showNewUserExistingUserControls && existingUserControls} + + setUserExisting(false)} + /> + {!showNewUserExistingUserControls && newUserControls} + + ); + + return isNewUser ? createUserControls : usernameAndEmailControls; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx index dbb47b50d4066..5f1fefc688c77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx @@ -9,15 +9,23 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; + +import { + REMOVE_ROLE_MAPPING_TITLE, + REMOVE_ROLE_MAPPING_BUTTON, + ROLE_MODAL_TEXT, +} from './constants'; import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; describe('UsersAndRolesRowActions', () => { const onManageClick = jest.fn(); const onDeleteClick = jest.fn(); + const username = 'foo'; const props = { + username, onManageClick, onDeleteClick, }; @@ -40,7 +48,19 @@ describe('UsersAndRolesRowActions', () => { const wrapper = shallow(); const button = wrapper.find(EuiButtonIcon).last(); button.simulate('click'); + wrapper.find(EuiConfirmModal).prop('onConfirm')!({} as any); expect(onDeleteClick).toHaveBeenCalled(); }); + + it('renders role mapping confirm modal text', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButtonIcon).last(); + button.simulate('click'); + const modal = wrapper.find(EuiConfirmModal); + + expect(modal.prop('title')).toEqual(REMOVE_ROLE_MAPPING_TITLE); + expect(modal.prop('children')).toEqual(

{ROLE_MODAL_TEXT}

); + expect(modal.prop('confirmButtonText')).toEqual(REMOVE_ROLE_MAPPING_BUTTON); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx index 3d956c0aabd68..a3b0d24769bf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx @@ -5,20 +5,65 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; -import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants'; +import { CANCEL_BUTTON_LABEL, MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants'; + +import { + REMOVE_ROLE_MAPPING_TITLE, + REMOVE_ROLE_MAPPING_BUTTON, + REMOVE_USER_BUTTON, + ROLE_MODAL_TEXT, + USER_MODAL_TITLE, + USER_MODAL_TEXT, +} from './constants'; interface Props { + username?: string; onManageClick(): void; onDeleteClick(): void; } -export const UsersAndRolesRowActions: React.FC = ({ onManageClick, onDeleteClick }) => ( - <> - {' '} - - -); +export const UsersAndRolesRowActions: React.FC = ({ + onManageClick, + onDeleteClick, + username, +}) => { + const [deleteModalVisible, setVisible] = useState(false); + const showDeleteModal = () => setVisible(true); + const closeDeleteModal = () => setVisible(false); + const title = username ? USER_MODAL_TITLE(username) : REMOVE_ROLE_MAPPING_TITLE; + const text = username ? USER_MODAL_TEXT : ROLE_MODAL_TEXT; + const confirmButton = username ? REMOVE_USER_BUTTON : REMOVE_ROLE_MAPPING_BUTTON; + + const deleteModal = ( + { + onDeleteClick(); + closeDeleteModal(); + }} + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={confirmButton} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

{text}

+
+ ); + + return ( + <> + {deleteModalVisible && deleteModal} + {' '} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx new file mode 100644 index 0000000000000..9110c09827c49 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { UsersEmptyPrompt } from './'; + +describe('UsersEmptyPrompt', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx new file mode 100644 index 0000000000000..42bf690c388c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiEmptyPrompt, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; + +import { docLinks } from '../doc_links'; + +import { NO_USERS_TITLE, NO_USERS_DESCRIPTION, ENABLE_USERS_LINK } from './constants'; + +const USERS_DOCS_URL = `${docLinks.enterpriseSearchBase}/users-access.html`; + +export const UsersEmptyPrompt: React.FC = () => ( + + + + + {NO_USERS_TITLE}} + body={

{NO_USERS_DESCRIPTION}

} + actions={ + + {ENABLE_USERS_LINK} + + } + /> +
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx new file mode 100644 index 0000000000000..9bae93079e89f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiText, EuiTitle } from '@elastic/eui'; + +import { UsersHeading } from './'; + +describe('UsersHeading', () => { + const onClick = jest.fn(); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiText)).toHaveLength(1); + expect(wrapper.find(EuiTitle)).toHaveLength(1); + }); + + it('handles button click', () => { + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx new file mode 100644 index 0000000000000..8d097e21e9c3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; + +import { USERS_HEADING_TITLE, USERS_HEADING_DESCRIPTION, USERS_HEADING_LABEL } from './constants'; + +interface Props { + onClick(): void; +} + +export const UsersHeading: React.FC = ({ onClick }) => ( + <> + + + +

{USERS_HEADING_TITLE}

+
+ +

{USERS_HEADING_DESCRIPTION}

+
+
+ + + {USERS_HEADING_LABEL} + + +
+ + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx new file mode 100644 index 0000000000000..dc1a2713ced12 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { asSingleUserRoleMapping, wsSingleUserRoleMapping, asRoleMapping } from './__mocks__/roles'; + +import React from 'react'; + +import { shallow, mount } from 'enzyme'; + +import { EuiInMemoryTable, EuiTextColor } from '@elastic/eui'; + +import { engines } from '../../app_search/__mocks__/engines.mock'; + +import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; + +import { UsersTable } from './'; + +describe('UsersTable', () => { + const initializeSingleUserRoleMapping = jest.fn(); + const handleDeleteMapping = jest.fn(); + const props = { + accessItemKey: 'groups' as 'groups' | 'engines', + singleUserRoleMappings: [wsSingleUserRoleMapping], + initializeSingleUserRoleMapping, + handleDeleteMapping, + }; + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + }); + + it('handles manage click', () => { + const wrapper = mount(); + wrapper.find(UsersAndRolesRowActions).prop('onManageClick')(); + + expect(initializeSingleUserRoleMapping).toHaveBeenCalled(); + }); + + it('handles delete click', () => { + const wrapper = mount(); + wrapper.find(UsersAndRolesRowActions).prop('onDeleteClick')(); + + expect(handleDeleteMapping).toHaveBeenCalled(); + }); + + it('handles display when no email present', () => { + const userWithNoEmail = { + ...wsSingleUserRoleMapping, + elasticsearchUser: { + email: null, + username: 'foo', + }, + }; + const wrapper = mount(); + + expect(wrapper.find(EuiTextColor)).toHaveLength(1); + }); + + it('handles access items display for all items', () => { + const userWithAllItems = { + ...asSingleUserRoleMapping, + roleMapping: { + ...asRoleMapping, + engines: [], + }, + }; + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="AllItems"]')).toHaveLength(1); + }); + + it('handles access items display more than 2 items', () => { + const extraEngine = { + ...engines[0], + id: '3', + }; + const userWithAllItems = { + ...asSingleUserRoleMapping, + roleMapping: { + ...asRoleMapping, + engines: [...engines, extraEngine], + }, + }; + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="AccessItems"]').prop('children')).toEqual( + `${engines[0].name}, ${engines[1].name} + 1` + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx new file mode 100644 index 0000000000000..86dc2c2626229 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiBadge, EuiBasicTableColumn, EuiInMemoryTable, EuiTextColor } from '@elastic/eui'; + +import { ASRoleMapping } from '../../app_search/types'; +import { SingleUserRoleMapping } from '../../shared/types'; +import { WSRoleMapping } from '../../workplace_search/types'; + +import { + INVITATION_PENDING_LABEL, + ALL_LABEL, + FILTER_USERS_LABEL, + NO_USERS_LABEL, + ROLE_LABEL, + USERNAME_LABEL, + EMAIL_LABEL, + GROUPS_LABEL, + ENGINES_LABEL, +} from './constants'; + +import { UsersAndRolesRowActions } from './'; + +interface AccessItem { + name: string; +} + +interface SharedUser extends SingleUserRoleMapping { + accessItems: AccessItem[]; + username: string; + email: string | null; + roleType: string; + id: string; +} + +interface SharedRoleMapping extends ASRoleMapping, WSRoleMapping { + accessItems: AccessItem[]; +} + +interface Props { + accessItemKey: 'groups' | 'engines'; + singleUserRoleMappings: Array>; + initializeSingleUserRoleMapping(roleId: string): string; + handleDeleteMapping(roleId: string): string; +} + +const noItemsPlaceholder = ; +const invitationBadge = {INVITATION_PENDING_LABEL}; + +export const UsersTable: React.FC = ({ + accessItemKey, + singleUserRoleMappings, + initializeSingleUserRoleMapping, + handleDeleteMapping, +}) => { + // 'accessItems' is needed because App Search has `engines` and Workplace Search has `groups`. + const users = ((singleUserRoleMappings as SharedUser[]).map((user) => ({ + username: user.elasticsearchUser.username, + email: user.elasticsearchUser.email, + roleType: user.roleMapping.roleType, + id: user.roleMapping.id, + accessItems: (user.roleMapping as SharedRoleMapping)[accessItemKey], + invitation: user.invitation, + })) as unknown) as Array>; + + const columns: Array> = [ + { + field: 'username', + name: USERNAME_LABEL, + render: (_, { username }: SharedUser) => username, + }, + { + field: 'email', + name: EMAIL_LABEL, + render: (_, { email, invitation }: SharedUser) => { + if (!email) return noItemsPlaceholder; + return ( +
+ {email} {invitation && invitationBadge} +
+ ); + }, + }, + { + field: 'roleType', + name: ROLE_LABEL, + render: (_, user: SharedUser) => user.roleType, + }, + { + field: 'accessItems', + name: accessItemKey === 'groups' ? GROUPS_LABEL : ENGINES_LABEL, + render: (_, { accessItems }: SharedUser) => { + // Design calls for showing the first 2 items followed by a +x after those 2. + // ['foo', 'bar', 'baz'] would display as: "foo, bar + 1" + const numItems = accessItems.length; + if (numItems === 0) return {ALL_LABEL}; + const additionalItems = numItems > 2 ? ` + ${numItems - 2}` : ''; + const names = accessItems.map((item) => item.name); + return ( + {names.slice(0, 2).join(', ') + additionalItems} + ); + }, + }, + { + field: 'id', + name: '', + render: (_, { id, username }: SharedUser) => ( + initializeSingleUserRoleMapping(id)} + onDeleteClick={() => handleDeleteMapping(id)} + /> + ), + }, + ]; + + const pagination = { + hidePerPageOptions: true, + pageSize: 10, + }; + + const search = { + box: { + incremental: true, + fullWidth: false, + placeholder: FILTER_USERS_LABEL, + 'data-test-subj': 'UsersTableSearchInput', + }, + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 67208c63ddf4c..e6d2c67d1baf8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -40,3 +40,19 @@ export interface RoleMapping { const productNames = [APP_SEARCH_PLUGIN.NAME, WORKPLACE_SEARCH_PLUGIN.NAME] as const; export type ProductName = typeof productNames[number]; + +export interface Invitation { + email: string; + code: string; +} + +export interface ElasticsearchUser { + email: string | null; + username: string; +} + +export interface SingleUserRoleMapping { + invitation: Invitation; + elasticsearchUser: ElasticsearchUser; + roleMapping: T; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx index a4eb228eff92f..050aaf1dadf89 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx @@ -11,8 +11,8 @@ import { useValues } from 'kea'; import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui'; import { Pager } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { USERNAME_LABEL, EMAIL_LABEL } from '../../../../shared/constants'; import { TableHeader } from '../../../../shared/table_header'; import { AppLogic } from '../../../app_logic'; import { UserRow } from '../../../components/shared/user_row'; @@ -20,27 +20,15 @@ import { User } from '../../../types'; import { GroupLogic } from '../group_logic'; const USERS_PER_PAGE = 10; -const USERNAME_TABLE_HEADER = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader', - { - defaultMessage: 'Username', - } -); -const EMAIL_TABLE_HEADER = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader', - { - defaultMessage: 'Email', - } -); export const GroupUsersTable: React.FC = () => { const { isFederatedAuth } = useValues(AppLogic); const { group: { users }, } = useValues(GroupLogic); - const headerItems = [USERNAME_TABLE_HEADER]; + const headerItems = [USERNAME_LABEL]; if (!isFederatedAuth) { - headerItems.push(EMAIL_TABLE_HEADER); + headerItems.push(EMAIL_LABEL); } const [firstItem, setFirstItem] = useState(0); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts index 92c8b7827b9b6..809b631c78391 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -7,14 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage', - { - defaultMessage: - 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', - } -); - export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index b153d01224193..01d32bec14ebd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -10,9 +10,14 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; +import { + RoleMappingsTable, + RoleMappingsHeading, + RolesEmptyPrompt, +} from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { WorkplaceSearchPageTemplate } from '../../components/layout'; +import { SECURITY_DOCS_URL } from '../../routes'; import { ROLE_MAPPINGS_TABLE_HEADER } from './constants'; @@ -20,9 +25,12 @@ import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; export const RoleMappings: React.FC = () => { - const { initializeRoleMappings, initializeRoleMapping, handleDeleteMapping } = useActions( - RoleMappingsLogic - ); + const { + enableRoleBasedAccess, + initializeRoleMappings, + initializeRoleMapping, + handleDeleteMapping, + } = useActions(RoleMappingsLogic); const { roleMappings, @@ -35,10 +43,19 @@ export const RoleMappings: React.FC = () => { initializeRoleMappings(); }, []); + const rolesEmptyState = ( + + ); + const roleMappingsSection = (
initializeRoleMapping()} /> { pageChrome={[ROLE_MAPPINGS_TITLE]} pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }} isLoading={dataLoading} + isEmptyState={roleMappings.length < 1} + emptyState={rolesEmptyState} > {roleMappingFlyoutOpen && } {roleMappingsSection} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index 4ee530870284e..a4bbddbd23b49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -90,6 +90,13 @@ describe('RoleMappingsLogic', () => { expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id])); }); + it('setRoleMappings', () => { + RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [wsRoleMapping] }); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + }); + it('handleRoleChange', () => { RoleMappingsLogic.actions.handleRoleChange('user'); @@ -234,6 +241,30 @@ describe('RoleMappingsLogic', () => { }); describe('listeners', () => { + describe('enableRoleBasedAccess', () => { + it('calls API and sets values', async () => { + const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings'); + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + + expect(RoleMappingsLogic.values.dataLoading).toEqual(true); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/role_mappings/enable_role_based_access' + ); + await nextTick(); + expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + describe('initializeRoleMappings', () => { it('calls API and sets values', async () => { const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); @@ -351,18 +382,8 @@ describe('RoleMappingsLogic', () => { }); describe('handleDeleteMapping', () => { - let confirmSpy: any; const roleMappingId = 'r1'; - beforeEach(() => { - confirmSpy = jest.spyOn(window, 'confirm'); - confirmSpy.mockImplementation(jest.fn(() => true)); - }); - - afterEach(() => { - confirmSpy.mockRestore(); - }); - it('calls API and refreshes list', async () => { const initializeRoleMappingsSpy = jest.spyOn( RoleMappingsLogic.actions, @@ -388,15 +409,6 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); - - it('will do nothing if not confirmed', async () => { - RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping); - window.confirm = () => false; - RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); - - expect(http.delete).not.toHaveBeenCalled(); - await nextTick(); - }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 361425b7a78a1..76b41b2f383eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -20,7 +20,6 @@ import { AttributeName } from '../../../shared/types'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; import { - DELETE_ROLE_MAPPING_MESSAGE, ROLE_MAPPING_DELETED_MESSAGE, ROLE_MAPPING_CREATED_MESSAGE, ROLE_MAPPING_UPDATED_MESSAGE, @@ -57,10 +56,16 @@ interface RoleMappingsActions { initializeRoleMappings(): void; resetState(): void; setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping }; + setRoleMappings({ + roleMappings, + }: { + roleMappings: WSRoleMapping[]; + }): { roleMappings: WSRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; openRoleMappingFlyout(): void; closeRoleMappingFlyout(): void; setRoleMappingErrors(errors: string[]): { errors: string[] }; + enableRoleBasedAccess(): void; } interface RoleMappingsValues { @@ -88,6 +93,7 @@ export const RoleMappingsLogic = kea data, setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }), + setRoleMappings: ({ roleMappings }: { roleMappings: WSRoleMapping[] }) => ({ roleMappings }), setRoleMappingErrors: (errors: string[]) => ({ errors }), handleAuthProviderChange: (value: string[]) => ({ value }), handleRoleChange: (roleType: Role) => ({ roleType }), @@ -98,6 +104,7 @@ export const RoleMappingsLogic = kea ({ value }), handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }), + enableRoleBasedAccess: true, resetState: true, initializeRoleMappings: true, initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), @@ -111,13 +118,16 @@ export const RoleMappingsLogic = kea false, + setRoleMappings: () => false, resetState: () => true, + enableRoleBasedAccess: () => true, }, ], roleMappings: [ [], { setRoleMappingsData: (_, { roleMappings }) => roleMappings, + setRoleMappings: (_, { roleMappings }) => roleMappings, resetState: () => [], }, ], @@ -260,6 +270,17 @@ export const RoleMappingsLogic = kea ({ + enableRoleBasedAccess: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/role_mappings/enable_role_based_access'; + + try { + const response = await http.post(route); + actions.setRoleMappings(response); + } catch (e) { + flashAPIErrors(e); + } + }, initializeRoleMappings: async () => { const { http } = HttpLogic.values; const route = '/api/workplace_search/org/role_mappings'; @@ -279,14 +300,12 @@ export const RoleMappingsLogic = kea { diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts index 718597c12e9c5..7d9f08627516b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts @@ -7,7 +7,11 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; -import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings'; +import { + registerEnableRoleMappingsRoute, + registerRoleMappingsRoute, + registerRoleMappingRoute, +} from './role_mappings'; const roleMappingBaseSchema = { rules: { username: 'user' }, @@ -18,6 +22,29 @@ const roleMappingBaseSchema = { }; describe('role mappings routes', () => { + describe('POST /api/app_search/role_mappings/enable_role_based_access', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/role_mappings/enable_role_based_access', + }); + + registerEnableRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/role_mappings/enable_role_based_access', + }); + }); + }); + describe('GET /api/app_search/role_mappings', () => { let mockRouter: MockRouter; @@ -36,7 +63,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings', + path: '/as/role_mappings', }); }); }); @@ -59,7 +86,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings', + path: '/as/role_mappings', }); }); @@ -94,7 +121,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', }); }); @@ -129,7 +156,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts index 75724a3344d6d..da620be2ea950 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts @@ -17,6 +17,21 @@ const roleMappingBaseSchema = { authProvider: schema.arrayOf(schema.string()), }; +export function registerEnableRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/role_mappings/enable_role_based_access', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/role_mappings/enable_role_based_access', + }) + ); +} + export function registerRoleMappingsRoute({ router, enterpriseSearchRequestHandler, @@ -27,7 +42,7 @@ export function registerRoleMappingsRoute({ validate: false, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings', + path: '/as/role_mappings', }) ); @@ -39,7 +54,7 @@ export function registerRoleMappingsRoute({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings', + path: '/as/role_mappings', }) ); } @@ -59,7 +74,7 @@ export function registerRoleMappingRoute({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', }) ); @@ -73,12 +88,13 @@ export function registerRoleMappingRoute({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', }) ); } export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerEnableRoleMappingsRoute(dependencies); registerRoleMappingsRoute(dependencies); registerRoleMappingRoute(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts index a945866da5ef2..aa0e9983166c0 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts @@ -7,9 +7,36 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; -import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings'; +import { + registerOrgEnableRoleMappingsRoute, + registerOrgRoleMappingsRoute, + registerOrgRoleMappingRoute, +} from './role_mappings'; describe('role mappings routes', () => { + describe('POST /api/workplace_search/org/role_mappings/enable_role_based_access', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/role_mappings/enable_role_based_access', + }); + + registerOrgEnableRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/enable_role_based_access', + }); + }); + }); + describe('GET /api/workplace_search/org/role_mappings', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts index a0fcec63cbb27..cea7bcb311ce8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts @@ -17,6 +17,21 @@ const roleMappingBaseSchema = { authProvider: schema.arrayOf(schema.string()), }; +export function registerOrgEnableRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/workplace_search/org/role_mappings/enable_role_based_access', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/enable_role_based_access', + }) + ); +} + export function registerOrgRoleMappingsRoute({ router, enterpriseSearchRequestHandler, @@ -79,6 +94,7 @@ export function registerOrgRoleMappingRoute({ } export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerOrgEnableRoleMappingsRoute(dependencies); registerOrgRoleMappingsRoute(dependencies); registerOrgRoleMappingRoute(dependencies); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e246cd0681053..17c31b8cd115e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7521,7 +7521,6 @@ "xpack.enterpriseSearch.appSearch.credentials.title": "資格情報", "xpack.enterpriseSearch.appSearch.credentials.updateWarning": "既存の API キーはユーザー間で共有できます。このキーのアクセス権を変更すると、このキーにアクセスできるすべてのユーザーに影響します。", "xpack.enterpriseSearch.appSearch.credentials.updateWarningTitle": "十分ご注意ください!", - "xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage": "このマッピングを完全に削除しますか?このアクションは元に戻せません。一部のユーザーがアクセスを失う可能性があります。", "xpack.enterpriseSearch.appSearch.DEV_ROLE_TYPE_DESCRIPTION": "開発者はエンジンのすべての要素を管理できます。", "xpack.enterpriseSearch.appSearch.documentCreation.api.description": "{documentsApiLink}を使用すると、新しいドキュメントをエンジンに追加できるほか、ドキュメントの更新、IDによるドキュメントの取得、ドキュメントの削除が可能です。基本操作を説明するさまざまな{clientLibrariesLink}があります。", "xpack.enterpriseSearch.appSearch.documentCreation.api.example": "実行中のAPIを表示するには、コマンドラインまたはクライアントライブラリを使用して、次の要求の例で実験することができます。", @@ -7906,6 +7905,7 @@ "xpack.enterpriseSearch.appSearch.tokens.search.description": "エンドポイントのみの検索では、公開検索キーが使用されます。", "xpack.enterpriseSearch.appSearch.tokens.search.name": "公開検索キー", "xpack.enterpriseSearch.appSearch.tokens.update": "正常に API キーを更新しました。", + "xpack.enterpriseSearch.emailLabel": "メール", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "場所を問わず、何でも検索。組織を支える多忙なチームのために、パワフルでモダンな検索エクスペリエンスを簡単に導入できます。Webサイトやアプリ、ワークプレイスに事前調整済みの検索をすばやく追加しましょう。何でもシンプルに検索できます。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "エンタープライズサーチはまだKibanaインスタンスで構成されていません。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt": "エンタープライズ サーチの基本操作", @@ -7948,15 +7948,14 @@ "xpack.enterpriseSearch.roleMapping.attributeSelectorTitle": "属性マッピング", "xpack.enterpriseSearch.roleMapping.attributeValueLabel": "属性値", "xpack.enterpriseSearch.roleMapping.authProviderLabel": "認証プロバイダー", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton": "マッピングを削除", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingDescription": "マッピングの削除は永久的であり、元に戻すことはできません", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle": "このロールマッピングを削除", "xpack.enterpriseSearch.roleMapping.externalAttributeLabel": "外部属性", "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "ロールをフィルタリング...", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "個別の認証プロバイダーを選択", "xpack.enterpriseSearch.roleMapping.manageRoleMappingTitle": "ロールマッピングを管理", "xpack.enterpriseSearch.roleMapping.noResults.message": "の結果が見つかりません。", "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "ロールマッピングを追加", + "xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle": "このロールマッピングを削除", "xpack.enterpriseSearch.roleMapping.roleLabel": "ロール", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "ユーザーとロール", "xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "ロールマッピングの保存", @@ -7993,6 +7992,7 @@ "xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName}とKibanaは別のElasticsearchクラスターにあります", "xpack.enterpriseSearch.troubleshooting.standardAuth.description": "このプラグインは、{standardAuthLink}の{productName}を完全にはサポートしていません。{productName}で作成されたユーザーはKibanaアクセス権が必要です。Kibanaで作成されたユーザーは、ナビゲーションメニューに{productName}が表示されません。", "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "標準認証の{productName}はサポートされていません", + "xpack.enterpriseSearch.usernameLabel": "ユーザー名", "xpack.enterpriseSearch.workplaceSearch.accountNav.account.link": "マイアカウント", "xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link": "ログアウト", "xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link": "組織ダッシュボードに移動", @@ -8163,8 +8163,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "グループ", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "コンテンツソース", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "ユーザー", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader": "メール", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader": "ユーザー名", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "前回更新日時{updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated": "このグループのユーザーが正常に更新されました。", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "グループを管理", @@ -8264,7 +8262,6 @@ "xpack.enterpriseSearch.workplaceSearch.reset.button": "リセット", "xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription": "管理者は、コンテンツソース、グループ、ユーザー管理機能など、すべての組織レベルの設定に無制限にアクセスできます。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "デフォルト", - "xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "このマッピングを完全に削除しますか?このアクションは元に戻せません。一部のユーザーがアクセスを失う可能性があります。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "1つ以上の割り当てられたグループが必要です。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "グループアクセス", "xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "ユーザーの機能アクセスは検索インターフェースと個人設定管理に制限されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6a96769e2da1e..055ccbdde6ae8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7580,7 +7580,6 @@ "xpack.enterpriseSearch.appSearch.credentials.title": "凭据", "xpack.enterpriseSearch.appSearch.credentials.updateWarning": "现有 API 密钥可在用户之间共享。更改此密钥的权限将影响有权访问此密钥的所有用户。", "xpack.enterpriseSearch.appSearch.credentials.updateWarningTitle": "谨慎操作!", - "xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage": "确定要永久删除此映射?此操作不可逆转,且某些用户可能会失去访问权限。", "xpack.enterpriseSearch.appSearch.DEV_ROLE_TYPE_DESCRIPTION": "开发人员可以管理引擎的所有方面。", "xpack.enterpriseSearch.appSearch.documentCreation.api.description": "{documentsApiLink} 可用于将新文档添加到您的引擎、更新文档、按 ID 检索文档以及删除文档。有各种{clientLibrariesLink}可帮助您入门。", "xpack.enterpriseSearch.appSearch.documentCreation.api.example": "要了解如何使用 API,可以在下面通过命令行或客户端库试用示例请求。", @@ -7974,6 +7973,7 @@ "xpack.enterpriseSearch.appSearch.tokens.search.description": "公有搜索密钥仅用于搜索终端。", "xpack.enterpriseSearch.appSearch.tokens.search.name": "公有搜索密钥", "xpack.enterpriseSearch.appSearch.tokens.update": "成功更新 API 密钥。", + "xpack.enterpriseSearch.emailLabel": "电子邮件", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "随时随地进行全面搜索。为工作繁忙的团队轻松实现强大的现代搜索体验。将预先调整的搜索功能快速添加到您的网站、应用或工作区。全面搜索就是这么简单。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "企业搜索尚未在您的 Kibana 实例中配置。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt": "企业搜索入门", @@ -8016,9 +8016,7 @@ "xpack.enterpriseSearch.roleMapping.attributeSelectorTitle": "属性映射", "xpack.enterpriseSearch.roleMapping.attributeValueLabel": "属性值", "xpack.enterpriseSearch.roleMapping.authProviderLabel": "身份验证提供程序", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton": "删除映射", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingDescription": "请注意,删除映射是永久性的,无法撤消", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle": "移除此角色映射", "xpack.enterpriseSearch.roleMapping.externalAttributeLabel": "外部属性", "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "筛选角色......", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "选择单个身份验证提供程序", @@ -8027,6 +8025,7 @@ "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "添加角色映射", "xpack.enterpriseSearch.roleMapping.roleLabel": "角色", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "用户和角色", + "xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle": "移除此角色映射", "xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "保存角色映射", "xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel": "更新角色映射", "xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.correct": "字段名称只能包含小写字母、数字和下划线", @@ -8061,6 +8060,7 @@ "xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName} 和 Kibana 在不同的 Elasticsearch 集群中", "xpack.enterpriseSearch.troubleshooting.standardAuth.description": "此插件不完全支持使用 {standardAuthLink} 的 {productName}。{productName} 中创建的用户必须具有 Kibana 访问权限。Kibana 中创建的用户在导航菜单中将看不到 {productName}。", "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "不支持使用标准身份验证的 {productName}", + "xpack.enterpriseSearch.usernameLabel": "用户名", "xpack.enterpriseSearch.workplaceSearch.accountNav.account.link": "我的帐户", "xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link": "注销", "xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link": "前往组织仪表板", @@ -8231,8 +8231,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "组", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "内容源", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "用户", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader": "电子邮件", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader": "用户名", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "上次更新于 {updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated": "已成功更新此组的用户", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "管理组", @@ -8332,7 +8330,6 @@ "xpack.enterpriseSearch.workplaceSearch.reset.button": "重置", "xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription": "管理员对所有组织范围设置 (包括内容源、组和用户管理功能) 具有完全权限。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "默认", - "xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "确定要永久删除此映射?此操作不可逆转,且某些用户可能会失去访问权限。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "至少需要一个分配的组。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "组访问权限", "xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "用户的功能访问权限仅限于搜索界面和个人设置管理。", From 136d3617032526dcb396896da408791c1362cb39 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 23 Jun 2021 15:10:34 -0500 Subject: [PATCH 02/86] Upgrade EUI to v34.3.0 (#101334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eui to v34.1.0 * styled-components types * src snapshot updates * x-pack snapshot updates * eui to v34.2.0 * styled-components todo * src snapshot updates * x-pack snapshot updates * jest test updates * collapsible_nav * Hard-code global nav width for bottom bar’s (for now) * Update to eui v34.3.0 * flyout unmock * src flyout snapshots * remove duplicate euioverlaymask * xpack flyout snapshots * remove unused import * sidenavprops * attr updates * trial: flyout ownfocus * remove unused * graph selector * jest * jest * flyout ownFocus * saved objects flyout * console welcome flyout * timeline flyout * clean up * visible * colorpicker data-test-subj * selectors * selector * ts * selector * snapshot * Fix `use_security_solution_navigation` TS error * cypress Co-authored-by: cchaos Co-authored-by: Chandler Prall --- package.json | 2 +- .../collapsible_nav.test.tsx.snap | 3451 ++++++++--------- .../header/__snapshots__/header.test.tsx.snap | 989 +++-- .../chrome/ui/header/collapsible_nav.test.tsx | 4 - .../public/chrome/ui/header/header.test.tsx | 2 +- src/core/public/chrome/ui/header/header.tsx | 2 +- .../flyout_service.test.tsx.snap | 4 +- src/core/public/styles/_base.scss | 2 +- .../application/components/welcome_panel.tsx | 2 +- .../dashboard_empty_screen.test.tsx.snap | 8 +- .../__snapshots__/data_view.test.tsx.snap | 30 +- .../discover_grid_flyout.test.tsx | 4 +- .../__snapshots__/source_viewer.test.tsx.snap | 22 +- .../url/__snapshots__/url.test.tsx.snap | 8 +- .../header/__snapshots__/header.test.tsx.snap | 2 +- .../warning_call_out.test.tsx.snap | 98 +- .../inspector_panel.test.tsx.snap | 1 + .../__snapshots__/solution_nav.test.tsx.snap | 18 + .../solution_nav/solution_nav.tsx | 2 +- .../public/components/labs/labs_flyout.tsx | 51 +- .../__snapshots__/intro.test.tsx.snap | 26 +- .../not_found_errors.test.tsx.snap | 160 +- .../__snapshots__/flyout.test.tsx.snap | 3 + .../objects_table/components/flyout.tsx | 2 +- .../components/color_picker.test.tsx | 4 +- .../visualization_noresults.test.js.snap | 2 +- test/accessibility/apps/management.ts | 1 + .../apps/management/_import_objects.ts | 8 +- test/functional/page_objects/settings_page.ts | 4 + .../page_objects/visual_builder_page.ts | 2 +- .../Waterfall/ResponsiveFlyout.tsx | 15 +- .../asset_manager.stories.storyshot | 17 +- .../custom_element_modal.stories.storyshot | 32 +- .../datasource_component.stories.storyshot | 10 +- .../keyboard_shortcuts_doc.stories.storyshot | 2145 +++++----- .../saved_elements_modal.stories.storyshot | 12 +- .../__snapshots__/pdf_panel.stories.storyshot | 4 +- .../__snapshots__/settings.test.tsx.snap | 10 +- .../autoplay_settings.stories.storyshot | 12 +- .../toolbar_settings.stories.storyshot | 12 +- .../filebeat_config_flyout.tsx | 2 +- .../private_sources_sidebar.tsx | 1 - .../components/create_agent_policy.tsx | 11 +- .../extend_index_management.test.tsx.snap | 142 +- .../__snapshots__/policy_table.test.tsx.snap | 15 +- .../components/table_basic.test.tsx | 22 +- .../upload_license.test.tsx.snap | 96 +- .../action_edit/edit_action_flyout.tsx | 327 +- .../__snapshots__/checker_errors.test.js.snap | 54 +- .../__snapshots__/no_data.test.js.snap | 8 +- .../__snapshots__/page_loading.test.js.snap | 4 +- .../app/cases/create/flyout.test.tsx | 2 +- .../components/app/cases/create/flyout.tsx | 10 +- .../shared/page_template/page_template.tsx | 10 +- ...screen_capture_panel_content.test.tsx.snap | 18 +- .../report_info_button.test.tsx.snap | 356 +- .../privilege_summary/privilege_summary.tsx | 61 +- .../privilege_space_form.tsx | 116 +- .../roles_grid_page.test.tsx.snap | 34 +- .../__snapshots__/prompt_page.test.tsx.snap | 4 +- .../unauthenticated_page.test.tsx.snap | 2 +- .../reset_session_page.test.tsx.snap | 2 +- .../timelines/data_providers.spec.ts | 4 +- .../integration/timelines/pagination.spec.ts | 6 +- .../cypress/screens/timeline.ts | 8 +- .../cases/components/create/flyout.test.tsx | 2 +- .../public/cases/components/create/flyout.tsx | 10 +- .../exceptions/add_exception_comments.tsx | 2 +- .../index.test.tsx | 4 +- .../endpoint_hosts/view/details/index.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 11 +- .../timelines/components/flyout/index.tsx | 11 +- .../components/flyout/pane/index.tsx | 13 +- .../__snapshots__/index.test.tsx.snap | 827 ++-- .../timelines/components/side_panel/index.tsx | 10 +- .../edit_transform_flyout.tsx | 127 +- .../sections/alert_form/alert_add.tsx | 1 + .../__snapshots__/license_info.test.tsx.snap | 30 +- .../ml/__snapshots__/ml_flyout.test.tsx.snap | 205 +- .../__snapshots__/expanded_row.test.tsx.snap | 82 +- .../waterfall/waterfall_flyout.tsx | 10 +- .../test/functional/apps/lens/lens_tagging.ts | 2 +- .../functional/page_objects/graph_page.ts | 4 +- .../test/functional/page_objects/lens_page.ts | 4 +- .../page_objects/space_selector_page.ts | 4 +- .../page_objects/tag_management_page.ts | 5 +- .../functional/tests/dashboard_integration.ts | 2 +- .../functional/tests/maps_integration.ts | 2 +- .../functional/tests/visualize_integration.ts | 2 +- yarn.lock | 25 +- 90 files changed, 4774 insertions(+), 5120 deletions(-) diff --git a/package.json b/package.json index 26465133569cd..f99eb86a43cec 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", - "@elastic/eui": "33.0.0", + "@elastic/eui": "34.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 3668829a6888c..0b10209bc13e5 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -370,54 +370,62 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` isOpen={true} onClose={[Function]} > - - - - } - /> - - -
-
+ + +
+
+ +
-
-
-
- - - -
+ data-euiicon-type="home" + /> + + + Home + + + + + +
-
-
- - + +
+
+ + + + +

+ Recently viewed +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - - - -

- Recently viewed -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ recent 2 + + + + + +
- -
+
+
-
-
- -
-
- + + + +
+
+ +
-
- + + + + + +

+ Analytics +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-kibana" - iconType="logoKibana" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="kibana" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Analytics" + paddingSize="none" > - - - - - - -

- Analytics -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ dashboard + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Observability +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-observability" - iconType="logoObservability" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="observability" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Observability" + paddingSize="none" > - - - - - - -

- Observability -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ logs + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Security +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-securitySolution" - iconType="logoSecurity" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="securitySolution" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Security" + paddingSize="none" > - - - - - - -

- Security -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ siem + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Management +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-management" - iconType="managementApp" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="management" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Management" + paddingSize="none" > - - - - - - -

- Management -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ monitoring + + + + + +
- -
+
+
-
-
- + + + +
-
- - - -
+ canvas + + + + + +
- - - +
+
+ + +
-
- -
    - - - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
  • - -
  • -
    -
-
-
+ , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + +
    - - -
    - - - - - - - -
    - +
    + + +
    +
    + + `; @@ -2770,42 +2706,57 @@ exports[`CollapsibleNav renders the default nav 3`] = ` isOpen={false} onClose={[Function]} > - - -
    -
    + + + + + +

    + Recently viewed +

    +
    +
    +
    + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    + +
    + +
    + + + +
    +
    + - -
    +
    +
    -
    - -
    - -
    -

    - No recently viewed items -

    -
    -
    -
    -
    -
    +

    + No recently viewed items +

    +
    + +
    +
    -
    -
    + + -
    - - -
    -
    - + + + +
    +
    + +
    -
    - - + +
    -
    - -
      - - - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • -
      -
    -
    -
    + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + +
    - - -
    - - - - - - - -
    - +
    + + + +
    + + `; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 6ad1e2d3a1cc6..5aee9ca1b7c08 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -4947,42 +4947,57 @@ exports[`Header renders 1`] = ` isOpen={false} onClose={[Function]} > - - -
    -
    +
    + +
    +
    + +
    -
    -
    -
    - - - -
    + data-euiicon-type="home" + /> + + + Home + + + + + +
    -
    -
    - - + +
    +
    + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    + +
    + +
    + + + +
    +
    + - -
    +
    +
    -
    - - - -
    + dashboard + + + + + +
    - -
    +
    +
    -
    -
    - -
    -
    - + + + +
    +
    + +
    -
    - +
    + +
      + +
    • + +
    • +
      +
    +
    +
    +
    + + +
    + + + Undock navigation + + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" onClick={[Function]} - size="s" + size="xs" >
  • @@ -5445,163 +5540,11 @@ exports[`Header renders 1`] = `
    - - -
    -
    - -
      - - - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • -
      -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - + + +
    + +
    diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 7f338a859e7b4..460770744d53a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -16,10 +16,6 @@ import { httpServiceMock } from '../../../http/http_service.mock'; import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; import { CollapsibleNav } from './collapsible_nav'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => 'mockId', -})); - const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; function mockLink({ title = 'discover', category }: Partial) { diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index fdbdde8556eeb..a3a0197b4017e 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -99,7 +99,7 @@ describe('Header', () => { act(() => isLocked$.next(true)); component.update(); - expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy(); + expect(component.find('[data-test-subj="collapsibleNav"]').exists()).toBeTruthy(); expect(component).toMatchSnapshot(); act(() => diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 67cdd24aae848..246ca83ef5ade 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -87,6 +87,7 @@ export function Header({ const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); const [isNavOpen, setIsNavOpen] = useState(false); + const [navId] = useState(htmlIdGenerator()()); const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$); if (!isVisible) { @@ -99,7 +100,6 @@ export function Header({ } const toggleCollapsibleNavRef = createRef void }>(); - const navId = htmlIdGenerator()(); const className = classnames('hide-for-sharing', 'headerGlobalNav'); const Breadcrumbs = ( diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index f5a1c51ccbe15..fbd09f3096854 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -26,7 +26,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 3386fa73f328a..de138cdf402e6 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -26,7 +26,7 @@ } .euiBody--collapsibleNavIsDocked .euiBottomBar { - margin-left: $euiCollapsibleNavWidth; + margin-left: 320px; // Hard-coded for now -- @cchaos } // Temporary fix for EuiPageHeader with a bottom border but no tabs or padding diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx index eb746e313d228..8514d41c04a51 100644 --- a/src/plugins/console/public/application/components/welcome_panel.tsx +++ b/src/plugins/console/public/application/components/welcome_panel.tsx @@ -27,7 +27,7 @@ interface Props { export function WelcomePanel(props: Props) { return ( - +

    diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 9f56740fdac22..afe339f3f43a2 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -603,7 +603,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` } > -
    -
    +
    @@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` } > -
    -
    +
    diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index a0a7e54d27532..0ab3f8a4e3466 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -176,27 +176,27 @@ exports[`Inspector Data View component should render empty state 1`] = `
    + +

    + + No data available + +

    +
    - -

    - - No data available - -

    -
    diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index 60841799b1398..50be2473a441e 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -144,7 +144,9 @@ describe('Discover flyout', function () { expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4'); }); - it('allows navigating with arrow keys through documents', () => { + // EuiFlyout is mocked in Jest environments. + // EUI team to reinstate `onKeyDown`: https://github.com/elastic/eui/issues/4883 + it.skip('allows navigating with arrow keys through documents', () => { const props = getProps(); const component = mountWithIntl(); findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap index f40dbbbae1f87..68786871825ac 100644 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -147,27 +147,27 @@ exports[`Source Viewer component renders error state 1`] = ` />
    + +

    + An Error Occurred +

    +
    - -

    - An Error Occurred -

    -
    diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index 40170c39942e5..79c1a11cfef84 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -153,7 +153,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormControlLayout__childrenWrapper" > diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 5ad8205365146..67d2cf72c5375 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -329,6 +329,7 @@ exports[`InspectorPanel should render as expected 1`] = ` >
    & { +export type KibanaPageTemplateSolutionNavProps = Partial> & { /** * Name of the solution, i.e. "Observability" */ diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx index 5b424c7e95f18..1af85da983085 100644 --- a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -20,7 +20,6 @@ import { EuiFlexItem, EuiFlexGroup, EuiIcon, - EuiOverlayMask, } from '@elastic/eui'; import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; @@ -124,30 +123,32 @@ export const LabsFlyout = (props: Props) => { ); return ( - onClose()} headerZindexLocation="below"> - - - -

    - - - - - {strings.getTitleLabel()} - -

    -
    - - -

    {strings.getDescriptionMessage()}

    -
    -
    - - - - {footer} -
    -
    + + + +

    + + + + + {strings.getTitleLabel()} + +

    +
    + + +

    {strings.getDescriptionMessage()}

    +
    +
    + + + + {footer} +
    ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap index 5239a92543539..5a8cd06b8ecc0 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap @@ -47,20 +47,30 @@ exports[`Intro component renders correctly 1`] = `
    -
    - +
    - Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. - -
    +
    + + Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. + +
    +
    +
    diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap index bddfe000008d4..f977c17df41d3 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap @@ -49,29 +49,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
    -
    - - The index pattern associated with this object no longer exists. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + The index pattern associated with this object no longer exists. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -128,29 +138,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
    -
    - - A field associated with this object no longer exists in the index pattern. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + A field associated with this object no longer exists in the index pattern. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -207,29 +227,39 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
    -
    - - The saved search associated with this object no longer exists. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + The saved search associated with this object no longer exists. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -286,21 +316,31 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
    -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index a68e8891b5ad1..bd97f2e6bffb1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -2,6 +2,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` @@ -277,6 +278,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` @@ -548,6 +550,7 @@ Array [ exports[`Flyout should render import step 1`] = ` diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 62e0cd0504e8e..f6c8d5fb69408 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -960,7 +960,7 @@ export class Flyout extends Component { } return ( - +

    diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx index 8e975f9904256..50d3e8c38e389 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx @@ -36,7 +36,7 @@ describe('ColorPicker', () => { const props = { ...defaultProps, value: '#68BC00' }; component = mount(); component.find('.tvbColorPicker button').simulate('click'); - const input = findTestSubject(component, 'topColorPickerInput'); + const input = findTestSubject(component, 'euiColorPickerInput_top'); expect(input.props().value).toBe('#68BC00'); }); @@ -44,7 +44,7 @@ describe('ColorPicker', () => { const props = { ...defaultProps, value: 'rgba(85,66,177,1)' }; component = mount(); component.find('.tvbColorPicker button').simulate('click'); - const input = findTestSubject(component, 'topColorPickerInput'); + const input = findTestSubject(component, 'euiColorPickerInput_top'); expect(input.props().value).toBe('85,66,177,1'); }); diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index 25ec05c83a8c6..56e2cb1b60f3c 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -14,7 +14,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` data-euiicon-type="visualizeApp" />
    { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); + await PageObjects.settings.clickCloseEditFieldFormatFlyout(); }); it('Advanced settings', async () => { diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 0278955c577a1..6ef0bfd5a09e8 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -419,14 +419,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'index-pattern-test-1' ); - await testSubjects.click('pagination-button-next'); + const flyout = await testSubjects.find('importSavedObjectsFlyout'); + + await (await flyout.findByTestSubject('pagination-button-next')).click(); await PageObjects.savedObjects.setOverriddenIndexPatternValue( 'missing-index-pattern-7', 'index-pattern-test-2' ); - await testSubjects.click('pagination-button-previous'); + await (await flyout.findByTestSubject('pagination-button-previous')).click(); const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( 'managementChangeIndexSelection-missing-index-pattern-1', @@ -435,7 +437,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); - await testSubjects.click('pagination-button-next'); + await (await flyout.findByTestSubject('pagination-button-next')).click(); const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( 'managementChangeIndexSelection-missing-index-pattern-7', diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 88951bb04c956..cb8f198177017 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -739,6 +739,10 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('editFieldFormat'); } + async clickCloseEditFieldFormatFlyout() { + await this.testSubjects.click('euiFlyoutCloseButton'); + } + async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) { await this.find.clickByCssSelector( `select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] > diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 6e263dd1cdbbf..7f1ea64bcd979 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -563,7 +563,7 @@ export class VisualBuilderPageObject extends FtrService { public async checkColorPickerPopUpIsPresent(): Promise { this.log.debug(`Check color picker popup is present`); - await this.testSubjects.existOrFail('colorPickerPopover', { timeout: 5000 }); + await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 }); } public async changePanelPreview(nth: number = 0): Promise { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx index 8549f09bba248..09fbf07b8ecbd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx @@ -5,10 +5,21 @@ * 2.0. */ +import { ReactNode } from 'react'; +import { StyledComponent } from 'styled-components'; import { EuiFlyout } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; +import { + euiStyled, + EuiTheme, +} from '../../../../../../../../../../src/plugins/kibana_react/common'; -export const ResponsiveFlyout = euiStyled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +export const ResponsiveFlyout: StyledComponent< + typeof EuiFlyout, + EuiTheme, + { children?: ReactNode } +> = euiStyled(EuiFlyout)` width: 100%; @media (min-width: 800px) { diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index 34b6b333f3ef5..d567d3cf85f13 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -116,20 +116,13 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` size="xxl" />
    - -

    - Import your assets to get started -

    -
    - + Import your assets to get started +

    diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot index 18f86aca24302..dc66eef809050 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot @@ -80,7 +80,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormControlLayout__childrenWrapper" >
    40 characters remaining
    @@ -119,7 +119,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormRow__fieldWrapper" >