Skip to content

Commit

Permalink
[Enterprise Search] Add notices for deactivated users and SMTP callout (
Browse files Browse the repository at this point in the history
elastic#103285)

* Port elastic#3904 to Kibana

elastic/ent-search#3904

* DRY out logic interfaces

Should have done this long ago

* Port elastic#3920 to Kibana

elastic/ent-search#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 <[email protected]>

* Remove manual disabling of other button

* Lint fixes

Co-authored-by: Constance <[email protected]>
  • Loading branch information
2 people authored and kibanamachine committed Jun 24, 2021
1 parent 00c2047 commit eadd6d7
Show file tree
Hide file tree
Showing 27 changed files with 406 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const RoleMapping: React.FC = () => {
selectedEngines,
selectedAuthProviders,
roleMappingErrors,
formLoading,
} = useValues(RoleMappingsLogic);

const isNew = !roleMapping;
Expand All @@ -67,6 +68,7 @@ export const RoleMapping: React.FC = () => {
return (
<RoleMappingFlyout
disabled={attributeValueInvalid || !hasEngineAssignment}
formLoading={formLoading}
isNew={isNew}
closeUsersAndRolesFlyout={closeUsersAndRolesFlyout}
handleSaveMapping={handleSaveMapping}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ describe('RoleMappingsLogic', () => {
userCreated: false,
userFormIsNewUser: true,
userFormUserIsExisting: true,
smtpSettingsPresent: false,
formLoading: false,
};

const mappingsServerProps = {
Expand All @@ -70,6 +72,7 @@ describe('RoleMappingsLogic', () => {
hasAdvancedRoles: false,
singleUserRoleMappings: [asSingleUserRoleMapping],
elasticsearchUsers,
smtpSettingsPresent: false,
};

beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,16 +32,11 @@ import {

type UserMapping = SingleUserRoleMapping<ASRoleMapping>;

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) =>
Expand All @@ -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({
Expand All @@ -73,48 +54,22 @@ 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;
singleUserRoleMappings: UserMapping[];
roleType: RoleTypes;
selectedAuthProviders: string[];
selectedEngines: Set<string>;
roleMappingFlyoutOpen: boolean;
singleUserRoleMappingFlyoutOpen: boolean;
selectedOptions: EuiComboBoxOptionOption[];
roleMappingErrors: string[];
userFormUserIsExisting: boolean;
userCreated: boolean;
userFormIsNewUser: boolean;
hasAdvancedRoles: boolean;
}

export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappingsActions>>({
Expand Down Expand Up @@ -369,6 +324,21 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi
setUserFormIsNewUser: (_, { userFormIsNewUser }) => userFormIsNewUser,
},
],
smtpSettingsPresent: [
false,
{
setRoleMappingsData: (_, { smtpSettingsPresent }) => smtpSettingsPresent,
},
],
formLoading: [
false,
{
handleSaveMapping: () => true,
handleSaveUser: () => true,
initializeRoleMappings: () => false,
setRoleMappingErrors: () => false,
},
],
},
selectors: ({ selectors }) => ({
selectedOptions: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(<User />);

expect(wrapper.find(DeactivatedUserCallout)).toHaveLength(1);
});

it('disables form when username value not present', () => {
setMockValues({
...mockValues,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
UserSelector,
UserAddedInfo,
UserInvitationCallout,
DeactivatedUserCallout,
} from '../../../shared/role_mapping';
import { RoleTypes } from '../../types';

Expand Down Expand Up @@ -48,13 +49,20 @@ export const User: React.FC = () => {
roleMappingErrors,
userCreated,
userFormIsNewUser,
smtpSettingsPresent,
formLoading,
} = useValues(RoleMappingsLogic);

const roleTypes = hasAdvancedRoles ? [...standardRoles, ...advancedRoles] : standardRoles;
const hasEngines = availableEngines.length > 0;
const showEngineAssignmentSelector = hasEngines && hasAdvancedRoles;
const flyoutDisabled =
!userFormUserIsExisting && (!elasticsearchUser.email || !elasticsearchUser.username);
const userIsDeactivated = !!(
singleUserRoleMapping &&
!singleUserRoleMapping.invitation &&
!singleUserRoleMapping.elasticsearchUser.enabled
);

const userAddedInfo = singleUserRoleMapping && (
<UserAddedInfo
Expand All @@ -76,6 +84,7 @@ export const User: React.FC = () => {
<EuiForm isInvalid={roleMappingErrors.length > 0} error={roleMappingErrors}>
<UserSelector
isNewUser={userFormIsNewUser}
smtpSettingsPresent={smtpSettingsPresent}
elasticsearchUsers={elasticsearchUsers}
handleRoleChange={handleRoleChange}
elasticsearchUser={elasticsearchUser}
Expand All @@ -94,13 +103,15 @@ export const User: React.FC = () => {
return (
<UserFlyout
disabled={flyoutDisabled}
formLoading={formLoading}
isComplete={userCreated}
isNew={userFormIsNewUser}
closeUserFlyout={closeUsersAndRolesFlyout}
handleSaveUser={handleSaveUser}
>
{userCreated ? userAddedInfo : createUserForm}
{userInvitationCallout}
{userIsDeactivated && <DeactivatedUserCallout isNew={userFormIsNewUser} />}
</UserFlyout>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export const elasticsearchUsers = [
{
email: '[email protected]',
username: 'user1',
enabled: true,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -166,5 +166,12 @@ describe('AttributeSelector', () => {
baseProps.elasticsearchRoles[0]
);
});

it('shows helpText when attributeValueInvalid', () => {
const wrapper = shallow(<AttributeSelector {...baseProps} attributeValueInvalid />);
const formRow = wrapper.find(EuiFormRow).at(2);

expect(formRow.prop('helpText')).toEqual(REQUIRED_LABEL);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -129,8 +129,7 @@ export const AttributeSelector: React.FC<Props> = ({
<EuiFormRow
label={ATTRIBUTE_VALUE_LABEL}
fullWidth
isInvalid={attributeValueInvalid}
error={[ATTRIBUTE_VALUE_ERROR]}
helpText={attributeValueInvalid && REQUIRED_LABEL}
>
{attributeName === 'role' ? (
<EuiSelect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,6 @@ export const ATTRIBUTE_VALUE_LABEL = i18n.translate(
}
);

export const ATTRIBUTE_VALUE_ERROR = i18n.translate(
'xpack.enterpriseSearch.roleMapping.attributeValueError',
{
defaultMessage: 'Attribute value is required',
}
);

export const REMOVE_ROLE_MAPPING_TITLE = i18n.translate(
'xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle',
{
Expand Down Expand Up @@ -373,6 +366,13 @@ export const UPDATE_USER_DESCRIPTION = i18n.translate(
}
);

export const DEACTIVATED_LABEL = i18n.translate(
'xpack.enterpriseSearch.roleMapping.deactivatedLabel',
{
defaultMessage: 'Deactivated',
}
);

export const INVITATION_PENDING_LABEL = i18n.translate(
'xpack.enterpriseSearch.roleMapping.invitationPendingLabel',
{
Expand Down Expand Up @@ -422,3 +422,29 @@ export const AUTH_PROVIDER_TOOLTIP = i18n.translate(
'Provider-specific role mapping is still applied, but configuration is now deprecated.',
}
);

export const DEACTIVATED_USER_CALLOUT_LABEL = i18n.translate(
'xpack.enterpriseSearch.roleMapping.deactivatedUserCalloutLabel',
{
defaultMessage: 'User deactivated',
}
);

export const DEACTIVATED_USER_CALLOUT_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.roleMapping.deactivatedUserCalloutDescription',
{
defaultMessage:
'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.',
}
);

export const SMTP_CALLOUT_LABEL = i18n.translate(
'xpack.enterpriseSearch.roleMapping.smtpCalloutLabel',
{
defaultMessage: 'Personalized invitations will be automatically sent when an Enterprise Search',
}
);

export const SMTP_LINK_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.smtpLinkLabel', {
defaultMessage: 'SMTP configuration is provided',
});
Loading

0 comments on commit eadd6d7

Please sign in to comment.