From eadd6d782144c371ef282cc1ef9b99ffd5943b15 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 24 Jun 2021 18:53:03 -0500 Subject: [PATCH] [Enterprise Search] Add notices for deactivated users and SMTP callout (#103285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Port #3904 to Kibana https://github.com/elastic/ent-search/pull/3904 * DRY out logic interfaces Should have done this long ago * Port #3920 to Kibana https://github.com/elastic/ent-search/pull/3920 * Lint fixes * Remove error state from form We already did this for the users flyout. Basically changes the dirty state of the form from an error state to just showing “Required”. i18n had not been translated yet for `ATTRIBUTE_VALUE_ERROR` * Add loading states * Remove manual disabling of button Co-authored-by: Constance * Remove manual disabling of other button * Lint fixes Co-authored-by: Constance --- .../components/role_mappings/role_mapping.tsx | 2 + .../role_mappings/role_mappings_logic.test.ts | 3 + .../role_mappings/role_mappings_logic.ts | 86 ++++++------------- .../components/role_mappings/user.test.tsx | 24 +++++- .../components/role_mappings/user.tsx | 11 +++ .../__mocks__/elasticsearch_users.ts | 1 + .../role_mapping/attribute_selector.test.tsx | 11 ++- .../role_mapping/attribute_selector.tsx | 5 +- .../shared/role_mapping/constants.ts | 40 +++++++-- .../deactivated_user_callout.test.tsx | 75 ++++++++++++++++ .../role_mapping/deactivated_user_callout.tsx | 28 ++++++ .../applications/shared/role_mapping/index.ts | 2 + .../role_mapping/role_mapping_flyout.test.tsx | 1 + .../role_mapping/role_mapping_flyout.tsx | 3 + .../applications/shared/role_mapping/types.ts | 66 ++++++++++++++ .../shared/role_mapping/user_flyout.test.tsx | 1 + .../shared/role_mapping/user_flyout.tsx | 4 +- .../role_mapping/user_selector.test.tsx | 3 +- .../shared/role_mapping/user_selector.tsx | 18 +++- .../shared/role_mapping/users_table.test.tsx | 20 ++++- .../shared/role_mapping/users_table.tsx | 10 ++- .../public/applications/shared/types.ts | 3 +- .../views/role_mappings/role_mapping.tsx | 2 + .../role_mappings/role_mappings_logic.test.ts | 3 + .../role_mappings/role_mappings_logic.ts | 84 ++++++------------ .../views/role_mappings/user.test.tsx | 24 +++++- .../views/role_mappings/user.tsx | 11 +++ 27 files changed, 406 insertions(+), 135 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index dbebd8e46a219..02ff4c0ba1c3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -45,6 +45,7 @@ export const RoleMapping: React.FC = () => { selectedEngines, selectedAuthProviders, roleMappingErrors, + formLoading, } = useValues(RoleMappingsLogic); const isNew = !roleMapping; @@ -67,6 +68,7 @@ export const RoleMapping: React.FC = () => { return ( { userCreated: false, userFormIsNewUser: true, userFormUserIsExisting: true, + smtpSettingsPresent: false, + formLoading: false, }; const mappingsServerProps = { @@ -70,6 +72,7 @@ describe('RoleMappingsLogic', () => { hasAdvancedRoles: false, singleUserRoleMappings: [asSingleUserRoleMapping], elasticsearchUsers, + smtpSettingsPresent: false, }; beforeEach(() => { 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 0b57e1d08a294..fb574a3588989 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 @@ -7,14 +7,17 @@ import { kea, MakeLogicType } from 'kea'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - import { clearFlashMessages, flashAPIErrors, setSuccessMessage, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { + RoleMappingsBaseServerDetails, + RoleMappingsBaseActions, + RoleMappingsBaseValues, +} from '../../../shared/role_mapping'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { ASRoleMapping, RoleTypes } from '../../types'; @@ -29,16 +32,11 @@ import { type UserMapping = SingleUserRoleMapping; -interface RoleMappingsServerDetails { +interface RoleMappingsServerDetails extends RoleMappingsBaseServerDetails { roleMappings: ASRoleMapping[]; - attributes: string[]; - authProviders: string[]; availableEngines: Engine[]; - elasticsearchRoles: string[]; - elasticsearchUsers: ElasticsearchUser[]; - hasAdvancedRoles: boolean; - multipleAuthProvidersConfig: boolean; singleUserRoleMappings: UserMapping[]; + hasAdvancedRoles: boolean; } const getFirstAttributeName = (roleMapping: ASRoleMapping) => @@ -47,24 +45,7 @@ const getFirstAttributeValue = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][1] as AttributeName; const emptyUser = { username: '', email: '' } as ElasticsearchUser; -interface RoleMappingsActions { - handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; - handleAuthProviderChange(value: string[]): { value: string[] }; - handleAttributeSelectorChange( - value: AttributeName, - firstElasticsearchRole: string - ): { value: AttributeName; firstElasticsearchRole: string }; - handleAttributeValueChange(value: string): { value: string }; - handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; - handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; - handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; - handleUsernameSelectChange(username: string): { username: string }; - handleSaveMapping(): void; - handleSaveUser(): void; - initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeRoleMappings(): void; - resetState(): void; +interface RoleMappingsActions extends RoleMappingsBaseActions { setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping }; setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; setRoleMappings({ @@ -73,34 +54,14 @@ interface RoleMappingsActions { roleMappings: ASRoleMapping[]; }): { roleMappings: ASRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; - setElasticsearchUser( - elasticsearchUser?: ElasticsearchUser - ): { elasticsearchUser: ElasticsearchUser }; - openRoleMappingFlyout(): void; - openSingleUserRoleMappingFlyout(): void; - closeUsersAndRolesFlyout(): void; - setRoleMappingErrors(errors: string[]): { errors: string[] }; - enableRoleBasedAccess(): void; - setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; - setElasticsearchUsernameValue(username: string): { username: string }; - setElasticsearchEmailValue(email: string): { email: string }; - setUserCreated(): void; - setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; + handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; + handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; + handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; } -interface RoleMappingsValues { +interface RoleMappingsValues extends RoleMappingsBaseValues { accessAllEngines: boolean; - attributeName: AttributeName; - attributeValue: string; - attributes: string[]; - availableAuthProviders: string[]; availableEngines: Engine[]; - dataLoading: boolean; - elasticsearchRoles: string[]; - elasticsearchUsers: ElasticsearchUser[]; - elasticsearchUser: ElasticsearchUser; - hasAdvancedRoles: boolean; - multipleAuthProvidersConfig: boolean; roleMapping: ASRoleMapping | null; roleMappings: ASRoleMapping[]; singleUserRoleMapping: UserMapping | null; @@ -108,13 +69,7 @@ interface RoleMappingsValues { roleType: RoleTypes; selectedAuthProviders: string[]; selectedEngines: Set; - roleMappingFlyoutOpen: boolean; - singleUserRoleMappingFlyoutOpen: boolean; - selectedOptions: EuiComboBoxOptionOption[]; - roleMappingErrors: string[]; - userFormUserIsExisting: boolean; - userCreated: boolean; - userFormIsNewUser: boolean; + hasAdvancedRoles: boolean; } export const RoleMappingsLogic = kea>({ @@ -369,6 +324,21 @@ export const RoleMappingsLogic = kea userFormIsNewUser, }, ], + smtpSettingsPresent: [ + false, + { + setRoleMappingsData: (_, { smtpSettingsPresent }) => smtpSettingsPresent, + }, + ], + formLoading: [ + false, + { + handleSaveMapping: () => true, + handleSaveUser: () => true, + initializeRoleMappings: () => false, + setRoleMappingErrors: () => false, + }, + ], }, selectors: ({ selectors }) => ({ selectedOptions: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx index 88103532bd149..cec7f1541a31a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx @@ -14,7 +14,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { + UserFlyout, + UserAddedInfo, + UserInvitationCallout, + DeactivatedUserCallout, +} from '../../../shared/role_mapping'; import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; @@ -91,6 +96,23 @@ describe('User', () => { expect(wrapper.find(UserAddedInfo)).toHaveLength(1); }); + it('renders DeactivatedUserCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: { + ...wsSingleUserRoleMapping, + invitation: null, + elasticsearchUser: { + ...wsSingleUserRoleMapping.elasticsearchUser, + enabled: false, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(DeactivatedUserCallout)).toHaveLength(1); + }); + it('disables form when username value not present', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx index df231fac64df7..018d29706b05f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx @@ -17,6 +17,7 @@ import { UserSelector, UserAddedInfo, UserInvitationCallout, + DeactivatedUserCallout, } from '../../../shared/role_mapping'; import { RoleTypes } from '../../types'; @@ -48,6 +49,8 @@ export const User: React.FC = () => { roleMappingErrors, userCreated, userFormIsNewUser, + smtpSettingsPresent, + formLoading, } = useValues(RoleMappingsLogic); const roleTypes = hasAdvancedRoles ? [...standardRoles, ...advancedRoles] : standardRoles; @@ -55,6 +58,11 @@ export const User: React.FC = () => { const showEngineAssignmentSelector = hasEngines && hasAdvancedRoles; const flyoutDisabled = !userFormUserIsExisting && (!elasticsearchUser.email || !elasticsearchUser.username); + const userIsDeactivated = !!( + singleUserRoleMapping && + !singleUserRoleMapping.invitation && + !singleUserRoleMapping.elasticsearchUser.enabled + ); const userAddedInfo = singleUserRoleMapping && ( { 0} error={roleMappingErrors}> { return ( { > {userCreated ? userAddedInfo : createUserForm} {userInvitationCallout} + {userIsDeactivated && } ); }; 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 index 500f560675679..6d9365d63c320 100644 --- 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 @@ -9,5 +9,6 @@ export const elasticsearchUsers = [ { email: 'user1@user.com', username: 'user1', + enabled: true, }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index d19d090b6e706..8fed459b0a8dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -9,12 +9,12 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiComboBox, EuiFieldText } from '@elastic/eui'; +import { EuiComboBox, EuiFieldText, EuiFormRow } from '@elastic/eui'; import { AttributeName } from '../types'; import { AttributeSelector } from './attribute_selector'; -import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; +import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, REQUIRED_LABEL } from './constants'; const handleAttributeSelectorChange = jest.fn(); const handleAttributeValueChange = jest.fn(); @@ -166,5 +166,12 @@ describe('AttributeSelector', () => { baseProps.elasticsearchRoles[0] ); }); + + it('shows helpText when attributeValueInvalid', () => { + const wrapper = shallow(); + const formRow = wrapper.find(EuiFormRow).at(2); + + expect(formRow.prop('helpText')).toEqual(REQUIRED_LABEL); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index bedb6b3df90f2..e24bc03bea452 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -21,7 +21,7 @@ import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, ATTRIBUTE_VALUE_LABEL, - ATTRIBUTE_VALUE_ERROR, + REQUIRED_LABEL, AUTH_ANY_PROVIDER_LABEL, AUTH_INDIVIDUAL_PROVIDER_LABEL, AUTH_PROVIDER_LABEL, @@ -129,8 +129,7 @@ export const AttributeSelector: React.FC = ({ {attributeName === 'role' ? ( { + it('renders with new', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + + User deactivated + + + + + This user is not currently active, and access has been temporarily revoked. Users can be re-activated via the User Management area of the Kibana console. + + + + `); + }); + + it('renders with existing', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + + + User deactivated + + + + + This user is not currently active, and access has been temporarily revoked. Users can be re-activated via the User Management area of the Kibana console. + + + + `); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.tsx new file mode 100644 index 0000000000000..5b69420d169ce --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/deactivated_user_callout.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 { EuiSpacer, EuiText, EuiIcon } from '@elastic/eui'; + +interface Props { + isNew: boolean; +} + +import { DEACTIVATED_USER_CALLOUT_LABEL, DEACTIVATED_USER_CALLOUT_DESCRIPTION } from './constants'; + +export const DeactivatedUserCallout: React.FC = ({ isNew }) => ( + <> + {!isNew && } + + {DEACTIVATED_USER_CALLOUT_LABEL} + + + {DEACTIVATED_USER_CALLOUT_DESCRIPTION} + + +); 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 8096b86939ff3..c10fc5c9d8242 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 @@ -5,7 +5,9 @@ * 2.0. */ +export * from './types'; export { AttributeSelector } from './attribute_selector'; +export { DeactivatedUserCallout } from './deactivated_user_callout'; export { RolesEmptyPrompt } from './roles_empty_prompt'; export { RoleMappingsTable } from './role_mappings_table'; export { RoleOptionLabel } from './role_option_label'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx index ffcf5508233fc..651a46f5df85e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx @@ -26,6 +26,7 @@ describe('RoleMappingFlyout', () => { const props = { isNew: true, disabled: false, + formLoading: false, closeUsersAndRolesFlyout, handleSaveMapping, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx index 4416a2de28011..bcaf26ccf2cfc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx @@ -36,6 +36,7 @@ interface Props { children: React.ReactNode; isNew: boolean; disabled: boolean; + formLoading: boolean; closeUsersAndRolesFlyout(): void; handleSaveMapping(): void; } @@ -44,6 +45,7 @@ export const RoleMappingFlyout: React.FC = ({ children, isNew, disabled, + formLoading, closeUsersAndRolesFlyout, handleSaveMapping, }) => ( @@ -78,6 +80,7 @@ export const RoleMappingFlyout: React.FC = ({ { isNew: true, isComplete: false, disabled: false, + formLoading: false, closeUserFlyout, handleSaveUser, }; 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 index a3be5e295ddfe..8741a2b4517d3 100644 --- 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 @@ -28,6 +28,7 @@ interface Props { isNew: boolean; isComplete: boolean; disabled: boolean; + formLoading: boolean; closeUserFlyout(): void; handleSaveUser(): void; } @@ -49,6 +50,7 @@ export const UserFlyout: React.FC = ({ isNew, isComplete, disabled, + formLoading, closeUserFlyout, handleSaveUser, }) => { @@ -75,7 +77,7 @@ export const UserFlyout: React.FC = ({ {CANCEL_BUTTON_LABEL} - + {isNew ? ADD_USER_LABEL : UPDATE_USER_LABEL} 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 index 60bac97d09835..0aea55a51040c 100644 --- 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 @@ -34,6 +34,7 @@ describe('UserSelector', () => { const props = { isNewUser: true, + smtpSettingsPresent: false, userFormUserIsExisting: true, elasticsearchUsers, elasticsearchUser: elasticsearchUsers[0], @@ -101,7 +102,7 @@ describe('UserSelector', () => { {...props} userFormUserIsExisting={false} elasticsearchUsers={[]} - elasticsearchUser={{ email: '', username: '' }} + elasticsearchUser={{ email: '', username: '', enabled: true }} /> ); 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 index d65f97265f6a3..25aff5077c680 100644 --- 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 @@ -9,6 +9,7 @@ import React from 'react'; import { EuiFieldText, + EuiLink, EuiRadio, EuiFormRow, EuiSelect, @@ -21,6 +22,9 @@ import { ElasticsearchUser } from '../../shared/types'; import { Role as WSRole } from '../../workplace_search/types'; import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; +import { docLinks } from '../doc_links'; + +const SMTP_URL = `${docLinks.enterpriseSearchBase}/mailer-configuration.html`; import { NEW_USER_LABEL, @@ -28,12 +32,15 @@ import { USERNAME_NO_USERS_TEXT, REQUIRED_LABEL, ROLE_LABEL, + SMTP_CALLOUT_LABEL, + SMTP_LINK_LABEL, } from './constants'; type SharedRole = WSRole | ASRole; interface Props { isNewUser: boolean; + smtpSettingsPresent: boolean; userFormUserIsExisting: boolean; elasticsearchUsers: ElasticsearchUser[]; elasticsearchUser: ElasticsearchUser; @@ -48,6 +55,7 @@ interface Props { export const UserSelector: React.FC = ({ isNewUser, + smtpSettingsPresent, userFormUserIsExisting, elasticsearchUsers, elasticsearchUser, @@ -66,6 +74,14 @@ export const UserSelector: React.FC = ({ })); const hasElasticsearchUsers = elasticsearchUsers.length > 0; const showNewUserExistingUserControls = userFormUserIsExisting && hasElasticsearchUsers; + const smptHelpText = !smtpSettingsPresent && ( + <> + {SMTP_CALLOUT_LABEL}{' '} + + {SMTP_LINK_LABEL} + + + ); const roleSelect = ( @@ -80,7 +96,7 @@ export const UserSelector: React.FC = ({ ); const emailInput = ( - + { singleUserRoleMappings: [wsSingleUserRoleMapping], initializeSingleUserRoleMapping, handleDeleteMapping, + enabled: true, }; it('renders', () => { @@ -55,6 +56,7 @@ describe('UsersTable', () => { elasticsearchUser: { email: null, username: 'foo', + enabled: true, }, }; const wrapper = mount(); @@ -97,4 +99,20 @@ describe('UsersTable', () => { `${engines[0].name}, ${engines[1].name} + 1` ); }); + + it('renders deactivatedBadge', () => { + const disabledUser = { + ...wsSingleUserRoleMapping, + elasticsearchUser: { + email: 'email@user.com', + username: 'foo', + enabled: false, + }, + invitation: null, + }; + const wrapper = mount(); + const cell = wrapper.find('[data-test-subj="UsernameCell"]'); + + expect(cell.find(EuiBadge)).toHaveLength(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 index 674796775b1d3..25a9eee38f93f 100644 --- 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 @@ -15,6 +15,7 @@ import { WSRoleMapping } from '../../workplace_search/types'; import { INVITATION_PENDING_LABEL, + DEACTIVATED_LABEL, ALL_LABEL, FILTER_USERS_LABEL, NO_USERS_LABEL, @@ -37,6 +38,7 @@ interface SharedUser extends SingleUserRoleMapping—; const invitationBadge = {INVITATION_PENDING_LABEL}; +const deactivatedBadge = {DEACTIVATED_LABEL}; export const UsersTable: React.FC = ({ accessItemKey, @@ -63,6 +66,7 @@ export const UsersTable: React.FC = ({ const users = ((singleUserRoleMappings as SharedUser[]).map((user) => ({ username: user.elasticsearchUser.username, email: user.elasticsearchUser.email, + enabled: user.elasticsearchUser.enabled, roleType: user.roleMapping.roleType, id: user.roleMapping.id, accessItems: (user.roleMapping as SharedRoleMapping)[accessItemKey], @@ -73,7 +77,11 @@ export const UsersTable: React.FC = ({ { field: 'username', name: USERNAME_LABEL, - render: (_, { username }: SharedUser) => username, + render: (_, { username, invitation, enabled }: SharedUser) => ( +
+ {username} {!invitation && !enabled && deactivatedBadge} +
+ ), }, { field: 'email', 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 e6d2c67d1baf8..5a90dd2c4a6bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -49,10 +49,11 @@ export interface Invitation { export interface ElasticsearchUser { email: string | null; username: string; + enabled: boolean; } export interface SingleUserRoleMapping { - invitation: Invitation; + invitation: Invitation | null; elasticsearchUser: ElasticsearchUser; roleMapping: T; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index 20211d40d7010..734716af9f627 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -59,6 +59,7 @@ export const RoleMapping: React.FC = () => { selectedAuthProviders, roleMapping, roleMappingErrors, + formLoading, } = useValues(RoleMappingsLogic); const isNew = !roleMapping; @@ -69,6 +70,7 @@ export const RoleMapping: React.FC = () => { return ( { userCreated: false, userFormIsNewUser: true, userFormUserIsExisting: true, + smtpSettingsPresent: false, + formLoading: false, }; const roleGroup = { id: '123', @@ -76,6 +78,7 @@ describe('RoleMappingsLogic', () => { elasticsearchRoles: [], singleUserRoleMappings: [wsSingleUserRoleMapping], elasticsearchUsers, + smtpSettingsPresent: false, }; beforeEach(() => { 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 7f26c8738786c..6e7104964cdb7 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 @@ -7,14 +7,17 @@ import { kea, MakeLogicType } from 'kea'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - import { clearFlashMessages, flashAPIErrors, setSuccessMessage, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { + RoleMappingsBaseServerDetails, + RoleMappingsBaseActions, + RoleMappingsBaseValues, +} from '../../../shared/role_mapping'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; @@ -28,14 +31,9 @@ import { type UserMapping = SingleUserRoleMapping; -interface RoleMappingsServerDetails { +interface RoleMappingsServerDetails extends RoleMappingsBaseServerDetails { roleMappings: WSRoleMapping[]; - attributes: string[]; - authProviders: string[]; availableGroups: RoleGroup[]; - elasticsearchUsers: ElasticsearchUser[]; - elasticsearchRoles: string[]; - multipleAuthProvidersConfig: boolean; singleUserRoleMappings: UserMapping[]; } @@ -45,24 +43,8 @@ const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => Object.entries(roleMapping.rules)[0][1] as string; const emptyUser = { username: '', email: '' } as ElasticsearchUser; -interface RoleMappingsActions { - handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; - handleAuthProviderChange(value: string[]): { value: string[] }; - handleAttributeSelectorChange( - value: AttributeName, - firstElasticsearchRole: string - ): { value: AttributeName; firstElasticsearchRole: string }; - handleAttributeValueChange(value: string): { value: string }; - handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; - handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; - handleRoleChange(roleType: Role): { roleType: Role }; - handleUsernameSelectChange(username: string): { username: string }; - handleSaveMapping(): void; - handleSaveUser(): void; - initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; - initializeRoleMappings(): void; - resetState(): void; +interface RoleMappingsActions extends RoleMappingsBaseActions { + setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping }; setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; setRoleMappings({ @@ -71,34 +53,14 @@ interface RoleMappingsActions { roleMappings: WSRoleMapping[]; }): { roleMappings: WSRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; - setElasticsearchUser( - elasticsearchUser?: ElasticsearchUser - ): { elasticsearchUser: ElasticsearchUser }; - setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; - openRoleMappingFlyout(): void; - openSingleUserRoleMappingFlyout(): void; - closeUsersAndRolesFlyout(): void; - setRoleMappingErrors(errors: string[]): { errors: string[] }; - enableRoleBasedAccess(): void; - setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; - setElasticsearchUsernameValue(username: string): { username: string }; - setElasticsearchEmailValue(email: string): { email: string }; - setUserCreated(): void; - setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; + handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; + handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; + handleRoleChange(roleType: Role): { roleType: Role }; } -interface RoleMappingsValues { +interface RoleMappingsValues extends RoleMappingsBaseValues { includeInAllGroups: boolean; - attributeName: AttributeName; - attributeValue: string; - attributes: string[]; - availableAuthProviders: string[]; availableGroups: RoleGroup[]; - dataLoading: boolean; - elasticsearchRoles: string[]; - elasticsearchUsers: ElasticsearchUser[]; - elasticsearchUser: ElasticsearchUser; - multipleAuthProvidersConfig: boolean; roleMapping: WSRoleMapping | null; roleMappings: WSRoleMapping[]; singleUserRoleMapping: UserMapping | null; @@ -106,13 +68,6 @@ interface RoleMappingsValues { roleType: Role; selectedAuthProviders: string[]; selectedGroups: Set; - roleMappingFlyoutOpen: boolean; - singleUserRoleMappingFlyoutOpen: boolean; - selectedOptions: EuiComboBoxOptionOption[]; - roleMappingErrors: string[]; - userFormUserIsExisting: boolean; - userCreated: boolean; - userFormIsNewUser: boolean; } export const RoleMappingsLogic = kea>({ @@ -369,6 +324,21 @@ export const RoleMappingsLogic = kea userFormIsNewUser, }, ], + smtpSettingsPresent: [ + false, + { + setRoleMappingsData: (_, { smtpSettingsPresent }) => smtpSettingsPresent, + }, + ], + formLoading: [ + false, + { + handleSaveMapping: () => true, + handleSaveUser: () => true, + initializeRoleMappings: () => false, + setRoleMappingErrors: () => false, + }, + ], }, selectors: ({ selectors }) => ({ selectedOptions: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx index 32ee1a7f22875..d8e1fc160901f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx @@ -14,7 +14,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { + UserFlyout, + UserAddedInfo, + UserInvitationCallout, + DeactivatedUserCallout, +} from '../../../shared/role_mapping'; import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; @@ -90,6 +95,23 @@ describe('User', () => { expect(wrapper.find(UserAddedInfo)).toHaveLength(1); }); + it('renders DeactivatedUserCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: { + ...wsSingleUserRoleMapping, + invitation: null, + elasticsearchUser: { + ...wsSingleUserRoleMapping.elasticsearchUser, + enabled: false, + }, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(DeactivatedUserCallout)).toHaveLength(1); + }); + it('disables form when username value not present', () => { setMockValues({ ...mockValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx index bfb32ee31c121..6447f43e6ed4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx @@ -17,6 +17,7 @@ import { UserSelector, UserAddedInfo, UserInvitationCallout, + DeactivatedUserCallout, } from '../../../shared/role_mapping'; import { Role } from '../../types'; @@ -46,12 +47,19 @@ export const User: React.FC = () => { roleMappingErrors, userCreated, userFormIsNewUser, + smtpSettingsPresent, + formLoading, } = useValues(RoleMappingsLogic); const showGroupAssignmentSelector = availableGroups.length > 0; const hasAvailableUsers = elasticsearchUsers.length > 0; const flyoutDisabled = (!userFormUserIsExisting || !hasAvailableUsers) && !elasticsearchUser.username; + const userIsDeactivated = !!( + singleUserRoleMapping && + !singleUserRoleMapping.invitation && + !singleUserRoleMapping.elasticsearchUser.enabled + ); const userAddedInfo = singleUserRoleMapping && ( { 0} error={roleMappingErrors}> { return ( { > {userCreated ? userAddedInfo : createUserForm} {userInvitationCallout} + {userIsDeactivated && } ); };