mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Add ability in Fleet UI for admin to create new users without email invitations (#1261)
This commit is contained in:
parent
2c277ba28a
commit
2bb2bf2d5d
14 changed files with 347 additions and 216 deletions
8
changes/1261-admin-create-user-no-invite
Normal file
8
changes/1261-admin-create-user-no-invite
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
- Add `USERS_ADMIN` endpoint to `fleet/endpoints`
|
||||
- Add `createUserWithoutInvitation` action to `redux/nodes/entities/users`
|
||||
- Revise `UserManagementPage`
|
||||
- Revise `UserForm`
|
||||
- Update existing Cypress test
|
||||
- Remove `renderSmtpWarning` from `UserManagementPage`
|
||||
|
||||
Implements #369
|
||||
|
|
@ -16,6 +16,10 @@ describe("User invite and activation", () => {
|
|||
|
||||
cy.findByLabelText(/email/i).click().type("ash@example.com");
|
||||
|
||||
cy.get(".create-user-form__new-user-radios").within(() => {
|
||||
cy.findByRole("radio", { name: "Invite user" }).parent().click();
|
||||
});
|
||||
|
||||
cy.findByRole("button", { name: /^create$/i }).click();
|
||||
|
||||
// Ensure the email has been delivered
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
z-index: 6;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
width: 150px;
|
||||
width: 188px;
|
||||
right: 28px;
|
||||
left: unset;
|
||||
top: unset;
|
||||
|
|
|
|||
|
|
@ -27,8 +27,12 @@ const Radio = (props: IRadioProps): JSX.Element => {
|
|||
} = props;
|
||||
const wrapperClasses = classnames(baseClass, className);
|
||||
|
||||
const radioControlClass = classnames({
|
||||
[`disabled`]: disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<label htmlFor={id} className={wrapperClasses}>
|
||||
<label htmlFor={id} className={`${wrapperClasses} ${radioControlClass}`}>
|
||||
<span className={`${baseClass}__input`}>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export default {
|
|||
STATUS_LABEL_COUNTS: "/v1/fleet/host_summary",
|
||||
TARGETS: "/v1/fleet/targets",
|
||||
USERS: "/v1/fleet/users",
|
||||
USERS_ADMIN: "/v1/fleet/users/admin",
|
||||
UPDATE_USER_ADMIN: (id: number): string => {
|
||||
return `/v1/fleet/users/${id}/admin`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ export default (client) => {
|
|||
.authenticatedPost(client._endpoint(USERS), JSON.stringify(formData))
|
||||
.then((response) => helpers.addGravatarUrlToResource(response.user));
|
||||
},
|
||||
createUserWithoutInvitation: (formData) => {
|
||||
const { USERS_ADMIN } = endpoints;
|
||||
|
||||
return client
|
||||
.authenticatedPost(
|
||||
client._endpoint(USERS_ADMIN),
|
||||
JSON.stringify(formData)
|
||||
)
|
||||
.then((response) => helpers.addGravatarUrlToResource(response.user)); // TODO confirm
|
||||
},
|
||||
deleteSessions: (user) => {
|
||||
const { USER_SESSIONS } = endpoints;
|
||||
const endpoint = client._endpoint(USER_SESSIONS(user.id));
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import { isEqual } from "lodash";
|
|||
import { push } from "react-router-redux";
|
||||
import memoize from "memoize-one";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import Modal from "components/modals/Modal";
|
||||
import WarningBanner from "components/WarningBanner";
|
||||
import inviteInterface from "interfaces/invite";
|
||||
import configInterface from "interfaces/config";
|
||||
import userInterface from "interfaces/user";
|
||||
|
|
@ -28,6 +26,7 @@ import { generateTableHeaders, combineDataSets } from "./UsersTableConfig";
|
|||
import DeleteUserForm from "./components/DeleteUserForm";
|
||||
import ResetPasswordModal from "./components/ResetPasswordModal";
|
||||
import ResetSessionsModal from "./components/ResetSessionsModal";
|
||||
import { NewUserType } from "./components/UserForm/UserForm";
|
||||
|
||||
const baseClass = "user-management";
|
||||
|
||||
|
|
@ -148,28 +147,53 @@ export class UserManagementPage extends Component {
|
|||
|
||||
onCreateUserSubmit = (formData) => {
|
||||
const { dispatch, config } = this.props;
|
||||
// Do some data formatting adding `invited_by` for the request to be correct.
|
||||
const requestData = {
|
||||
...formData,
|
||||
invited_by: formData.currentUserId,
|
||||
};
|
||||
delete requestData.currentUserId; // dont need this for the request.
|
||||
dispatch(inviteActions.create(requestData))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"success",
|
||||
`An invitation email was sent from ${config.sender_address} to ${formData.email}.`
|
||||
)
|
||||
);
|
||||
this.toggleCreateUserModal();
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
);
|
||||
this.toggleCreateUserModal();
|
||||
});
|
||||
|
||||
if (formData.newUserType === NewUserType.AdminInvited) {
|
||||
// Do some data formatting adding `invited_by` for the request to be correct and deleteing uncessary fields
|
||||
const requestData = {
|
||||
...formData,
|
||||
invited_by: formData.currentUserId,
|
||||
};
|
||||
delete requestData.currentUserId; // this field is not needed for the request
|
||||
delete requestData.newUserType; // this field is not needed for the request
|
||||
delete requestData.password; // this field is not needed for the request
|
||||
dispatch(inviteActions.create(requestData))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
renderFlash(
|
||||
"success",
|
||||
`An invitation email was sent from ${config.sender_address} to ${formData.email}.`
|
||||
)
|
||||
);
|
||||
this.toggleCreateUserModal();
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
);
|
||||
this.toggleCreateUserModal();
|
||||
});
|
||||
} else {
|
||||
// Do some data formatting deleteing uncessary fields
|
||||
const requestData = {
|
||||
...formData,
|
||||
};
|
||||
delete requestData.currentUserId; // this field is not needed for the request
|
||||
delete requestData.newUserType; // this field is not needed for the request
|
||||
dispatch(userActions.createUserWithoutInvitation(requestData))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
renderFlash("success", `Successfully created ${requestData.name}.`)
|
||||
);
|
||||
this.toggleCreateUserModal();
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(
|
||||
renderFlash("error", "Could not create user. Please try again.")
|
||||
);
|
||||
this.toggleCreateUserModal();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onCreateCancel = (evt) => {
|
||||
|
|
@ -453,9 +477,11 @@ export class UserManagementPage extends Component {
|
|||
availableTeams={teams}
|
||||
defaultGlobalRole={"observer"}
|
||||
defaultTeams={[]}
|
||||
defaultNewUserType={false}
|
||||
submitText={"Create"}
|
||||
isBasicTier={isBasicTier}
|
||||
smtpConfigured={config.configured}
|
||||
isSmtpConfigured={config.configured}
|
||||
isNewUser
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
|
@ -514,34 +540,6 @@ export class UserManagementPage extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
renderSmtpWarning = () => {
|
||||
const { appConfigLoading, config } = this.props;
|
||||
const { goToAppConfigPage } = this;
|
||||
|
||||
if (appConfigLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__smtp-warning-wrapper`}>
|
||||
<WarningBanner shouldShowWarning={!config.configured}>
|
||||
<span>
|
||||
SMTP is not currently configured in Fleet. The "Create
|
||||
User" feature requires that SMTP is configured in order to send
|
||||
invitation emails.
|
||||
</span>
|
||||
<Button
|
||||
className={`${baseClass}__config-button`}
|
||||
onClick={goToAppConfigPage}
|
||||
variant={"unstyled"}
|
||||
>
|
||||
Configure SMTP
|
||||
</Button>
|
||||
</WarningBanner>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
tableHeaders,
|
||||
|
|
@ -550,18 +548,11 @@ export class UserManagementPage extends Component {
|
|||
renderDeleteUserModal,
|
||||
renderResetPasswordModal,
|
||||
renderResetSessionsModal,
|
||||
renderSmtpWarning,
|
||||
toggleCreateUserModal,
|
||||
onTableQueryChange,
|
||||
onActionSelect,
|
||||
} = this;
|
||||
const {
|
||||
config,
|
||||
loadingTableData,
|
||||
users,
|
||||
invites,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
const { loadingTableData, users, invites, currentUser } = this.props;
|
||||
|
||||
let tableData = [];
|
||||
if (!loadingTableData) {
|
||||
|
|
@ -579,7 +570,6 @@ export class UserManagementPage extends Component {
|
|||
Create new users, customize user permissions, and remove users from
|
||||
Fleet.
|
||||
</p>
|
||||
{renderSmtpWarning()}
|
||||
{/* TODO: find a way to move these controls into the table component */}
|
||||
<TableContainer
|
||||
columns={tableHeaders}
|
||||
|
|
@ -588,8 +578,7 @@ export class UserManagementPage extends Component {
|
|||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"desc"}
|
||||
inputPlaceHolder={"Search"}
|
||||
disableActionButton={!config.configured}
|
||||
actionButtonText={"Create User"}
|
||||
actionButtonText={"Create user"}
|
||||
onActionButtonClick={toggleCreateUserModal}
|
||||
onQueryChange={onTableQueryChange}
|
||||
resultsTitle={"users"}
|
||||
|
|
@ -599,7 +588,6 @@ export class UserManagementPage extends Component {
|
|||
{renderEditUserModal()}
|
||||
{renderDeleteUserModal()}
|
||||
{renderResetSessionsModal()}
|
||||
|
||||
{renderResetPasswordModal()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
import { mount } from "enzyme";
|
||||
|
||||
import { connectedComponent, reduxMockStore } from "test/helpers";
|
||||
import ConnectedUserManagementPage from "pages/admin/UserManagementPage/UserManagementPage";
|
||||
import inviteActions from "redux/nodes/entities/invites/actions";
|
||||
import userActions from "redux/nodes/entities/users/actions";
|
||||
|
||||
const currentUser = {
|
||||
email: "hi@gnar.dog",
|
||||
enabled: true,
|
||||
name: "Gnar Dog",
|
||||
position: "Head of Gnar",
|
||||
username: "gnardog",
|
||||
teams: [],
|
||||
global_role: "admin",
|
||||
};
|
||||
const store = {
|
||||
app: {
|
||||
config: {
|
||||
configured: true,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
user: {
|
||||
...currentUser,
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
users: {
|
||||
loading: false,
|
||||
data: {
|
||||
1: {
|
||||
...currentUser,
|
||||
},
|
||||
},
|
||||
originalOrder: [1],
|
||||
},
|
||||
invites: {
|
||||
loading: false,
|
||||
data: {
|
||||
1: {
|
||||
global_role: "admin",
|
||||
email: "other@user.org",
|
||||
name: "Other user",
|
||||
teams: [],
|
||||
},
|
||||
},
|
||||
originalOrder: [1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("UserManagementPage - component", () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(userActions, "loadAll")
|
||||
.mockImplementation(() => () => Promise.resolve([]));
|
||||
|
||||
jest
|
||||
.spyOn(inviteActions, "loadAll")
|
||||
.mockImplementation(() => () => Promise.resolve([]));
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
it('displays a disabled "Create user" button if email is not configured', () => {
|
||||
const notConfiguredStore = {
|
||||
...store,
|
||||
app: { config: { configured: false } },
|
||||
};
|
||||
const notConfiguredMockStore = reduxMockStore(notConfiguredStore);
|
||||
const notConfiguredPage = mount(
|
||||
connectedComponent(ConnectedUserManagementPage, {
|
||||
mockStore: notConfiguredMockStore,
|
||||
})
|
||||
);
|
||||
|
||||
const configuredStore = store;
|
||||
const configuredMockStore = reduxMockStore(configuredStore);
|
||||
const configuredPage = mount(
|
||||
connectedComponent(ConnectedUserManagementPage, {
|
||||
mockStore: configuredMockStore,
|
||||
})
|
||||
);
|
||||
|
||||
expect(notConfiguredPage.find("Button").at(1).prop("disabled")).toEqual(
|
||||
true
|
||||
);
|
||||
expect(configuredPage.find("Button").first().prop("disabled")).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("displays a SmtpWarning if email is not configured", () => {
|
||||
const notConfiguredStore = {
|
||||
...store,
|
||||
app: { config: { configured: false } },
|
||||
};
|
||||
const notConfiguredMockStore = reduxMockStore(notConfiguredStore);
|
||||
const notConfiguredPage = mount(
|
||||
connectedComponent(ConnectedUserManagementPage, {
|
||||
mockStore: notConfiguredMockStore,
|
||||
})
|
||||
);
|
||||
|
||||
const configuredStore = store;
|
||||
const configuredMockStore = reduxMockStore(configuredStore);
|
||||
const configuredPage = mount(
|
||||
connectedComponent(ConnectedUserManagementPage, {
|
||||
mockStore: configuredMockStore,
|
||||
})
|
||||
);
|
||||
|
||||
expect(notConfiguredPage.find("WarningBanner").html()).toBeTruthy();
|
||||
expect(configuredPage.find("WarningBanner").html()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("goes to the app settings page for the user to resolve their smtp settings", () => {
|
||||
const notConfiguredStore = {
|
||||
...store,
|
||||
app: { config: { configured: false } },
|
||||
};
|
||||
const mockStore = reduxMockStore(notConfiguredStore);
|
||||
const page = mount(
|
||||
connectedComponent(ConnectedUserManagementPage, { mockStore })
|
||||
);
|
||||
|
||||
const smtpWarning = page.find("WarningBanner");
|
||||
|
||||
smtpWarning.find("Button").simulate("click");
|
||||
|
||||
const goToAppSettingsAction = {
|
||||
type: "@@router/CALL_HISTORY_METHOD",
|
||||
payload: { method: "push", args: ["/settings/organization"] },
|
||||
};
|
||||
|
||||
expect(mockStore.getActions()).toContainEqual(goToAppSettingsAction);
|
||||
});
|
||||
});
|
||||
|
|
@ -209,7 +209,7 @@ const generateActionDropdownOptions = (
|
|||
value: "delete",
|
||||
},
|
||||
];
|
||||
if (isCurrentUser || isSSOEnabled) {
|
||||
if (isSSOEnabled) {
|
||||
// remove "Require password reset" from dropdownOptions
|
||||
dropdownOptions = dropdownOptions.filter(
|
||||
(option) => option.label !== "Require password reset"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ import validEmail from "components/forms/validators/valid_email";
|
|||
|
||||
// ignore TS error for now until these are rewritten in ts.
|
||||
// @ts-ignore
|
||||
import validPassword from "components/forms/validators/valid_password";
|
||||
// @ts-ignore
|
||||
import IconToolTip from "components/IconToolTip";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
|
||||
// @ts-ignore
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
|
|
@ -20,6 +26,11 @@ import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2
|
|||
|
||||
const baseClass = "create-user-form";
|
||||
|
||||
export enum NewUserType {
|
||||
AdminInvited = "ADMIN_INVITED",
|
||||
AdminCreated = "ADMIN_CREATED",
|
||||
}
|
||||
|
||||
enum UserTeamType {
|
||||
GlobalUser = "GLOBAL_USER",
|
||||
AssignTeams = "ASSIGN_TEAMS",
|
||||
|
|
@ -46,6 +57,8 @@ const globalUserRoles = [
|
|||
export interface IFormData {
|
||||
email: string;
|
||||
name: string;
|
||||
newUserType?: NewUserType | null;
|
||||
password?: string | null;
|
||||
sso_enabled: boolean;
|
||||
global_role: string | null;
|
||||
teams: ITeam[];
|
||||
|
|
@ -65,6 +78,8 @@ interface ICreateUserFormProps {
|
|||
defaultGlobalRole?: string | null;
|
||||
defaultTeams?: ITeam[];
|
||||
isBasicTier: boolean;
|
||||
isSmtpConfigured?: boolean;
|
||||
isNewUser?: boolean;
|
||||
validationErrors: any[]; // TODO: proper interface for validationErrors
|
||||
smtpConfigured: boolean;
|
||||
}
|
||||
|
|
@ -73,6 +88,7 @@ interface ICreateUserFormState {
|
|||
errors: {
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
password: string | null;
|
||||
sso_enabled: boolean | null;
|
||||
};
|
||||
formData: IFormData;
|
||||
|
|
@ -87,12 +103,17 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
errors: {
|
||||
email: null,
|
||||
name: null,
|
||||
password: null,
|
||||
sso_enabled: null,
|
||||
},
|
||||
formData: {
|
||||
email: props.defaultEmail || "",
|
||||
name: props.defaultName || "",
|
||||
sso_enabled: props.canUseSSO || false,
|
||||
newUserType: props.isNewUser ? NewUserType.AdminCreated : null,
|
||||
password: null,
|
||||
sso_enabled: props.isNewUser
|
||||
? props.canUseSSO || false
|
||||
: props.canUseSSO || false, // TODO revisit the else case for editing an existing user; shouldn't this be pulling from the user data instead of global app config?
|
||||
global_role: props.defaultGlobalRole || null,
|
||||
teams: props.defaultTeams || [],
|
||||
currentUserId: props.currentUserId,
|
||||
|
|
@ -126,6 +147,12 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
};
|
||||
};
|
||||
|
||||
onRadioChange = (formField: string): ((evt: string) => void) => {
|
||||
return (evt: string) => {
|
||||
return this.onInputChange(formField)(evt);
|
||||
};
|
||||
};
|
||||
|
||||
onIsGlobalUserChange = (value: string): void => {
|
||||
const { formData } = this.state;
|
||||
const isGlobalUser = value === UserTeamType.GlobalUser;
|
||||
|
|
@ -168,19 +195,39 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
}
|
||||
};
|
||||
|
||||
// UserForm component can be used to create a new user or edit an existing user so submitData will be assembled accordingly
|
||||
createSubmitData = (): IFormData => {
|
||||
const { currentUserId } = this.props;
|
||||
const { currentUserId, isNewUser } = this.props;
|
||||
const {
|
||||
isGlobalUser,
|
||||
formData: { email, name, sso_enabled, global_role, teams },
|
||||
formData: {
|
||||
email,
|
||||
name,
|
||||
newUserType,
|
||||
password,
|
||||
sso_enabled,
|
||||
global_role,
|
||||
teams,
|
||||
},
|
||||
} = this.state;
|
||||
|
||||
const submitData = {
|
||||
email,
|
||||
name,
|
||||
newUserType,
|
||||
password,
|
||||
sso_enabled,
|
||||
currentUserId,
|
||||
};
|
||||
|
||||
if (!isNewUser) {
|
||||
delete submitData.newUserType; // this field will not be submitted when form is used to edit an existing user
|
||||
}
|
||||
|
||||
if (submitData.sso_enabled || newUserType === NewUserType.AdminInvited) {
|
||||
delete submitData.password; // this field will not be submitted with the form
|
||||
}
|
||||
|
||||
return isGlobalUser
|
||||
? { ...submitData, global_role, teams: [] }
|
||||
: { ...submitData, global_role: null, teams };
|
||||
|
|
@ -189,8 +236,9 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
validate = (): boolean => {
|
||||
const {
|
||||
errors,
|
||||
formData: { email },
|
||||
formData: { email, password, newUserType, sso_enabled },
|
||||
} = this.state;
|
||||
const { isNewUser } = this.props;
|
||||
|
||||
if (!validatePresence(email)) {
|
||||
this.setState({
|
||||
|
|
@ -214,6 +262,29 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (isNewUser && newUserType === NewUserType.AdminCreated && !sso_enabled) {
|
||||
if (!validatePresence(password)) {
|
||||
this.setState({
|
||||
errors: {
|
||||
...errors,
|
||||
password: "Password field must be completed",
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
if (!validPassword(password)) {
|
||||
this.setState({
|
||||
errors: {
|
||||
...errors,
|
||||
password: "Password must meet the criteria below",
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -288,14 +359,21 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
render(): JSX.Element {
|
||||
const {
|
||||
errors,
|
||||
formData: { email, name, sso_enabled },
|
||||
formData: { email, name, newUserType, password, sso_enabled },
|
||||
isGlobalUser,
|
||||
} = this.state;
|
||||
const { onCancel, submitText, isBasicTier, smtpConfigured } = this.props;
|
||||
const {
|
||||
onCancel,
|
||||
submitText,
|
||||
isBasicTier,
|
||||
isSmtpConfigured,
|
||||
isNewUser,
|
||||
} = this.props;
|
||||
const {
|
||||
onFormSubmit,
|
||||
onInputChange,
|
||||
onCheckboxChange,
|
||||
onRadioChange,
|
||||
onIsGlobalUserChange,
|
||||
renderGlobalRoleForm,
|
||||
renderTeamsForm,
|
||||
|
|
@ -323,7 +401,7 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
className="smtp-not-configured"
|
||||
data-tip
|
||||
data-for="smtp-tooltip"
|
||||
data-tip-disable={smtpConfigured}
|
||||
data-tip-disable={isSmtpConfigured}
|
||||
>
|
||||
<InputFieldWithIcon
|
||||
error={errors.email}
|
||||
|
|
@ -331,7 +409,7 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
onChange={onInputChange("email")}
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
disabled={!smtpConfigured}
|
||||
disabled={!isSmtpConfigured}
|
||||
/>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
|
|
@ -365,6 +443,83 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
|
|||
Password authentication will be disabled for this user.
|
||||
</p>
|
||||
</div>
|
||||
{isNewUser && (
|
||||
<div className={`${baseClass}__new-user-container`}>
|
||||
<div className={`${baseClass}__new-user-radios`}>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Create user"}
|
||||
id={"create-user"}
|
||||
checked={newUserType !== NewUserType.AdminInvited}
|
||||
value={NewUserType.AdminCreated}
|
||||
name={"newUserType"}
|
||||
onChange={onRadioChange("newUserType")}
|
||||
/>
|
||||
<div
|
||||
className="smtp-not-configured"
|
||||
data-tip
|
||||
data-for="invite-user-tooltip"
|
||||
data-tip-disable={isSmtpConfigured}
|
||||
>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
label={"Invite user"}
|
||||
id={"invite-user"}
|
||||
disabled={!isSmtpConfigured}
|
||||
checked={newUserType === NewUserType.AdminInvited}
|
||||
value={NewUserType.AdminInvited}
|
||||
name={"newUserType"}
|
||||
onChange={onRadioChange("newUserType")}
|
||||
/>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id="invite-user-tooltip"
|
||||
backgroundColor="#3e4771"
|
||||
data-html
|
||||
>
|
||||
<span className={`${baseClass}__tooltip-text`}>
|
||||
The Invite user feature requires that SMTP is
|
||||
<br />
|
||||
configured in order to send an invitation email. <br />
|
||||
<br />
|
||||
SMTP can be configured in Organization settings.
|
||||
</span>
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</div>
|
||||
{newUserType !== NewUserType.AdminInvited && !sso_enabled && (
|
||||
<>
|
||||
<div className={`${baseClass}__password`}>
|
||||
<InputField
|
||||
error={errors.password}
|
||||
name="password"
|
||||
onChange={onInputChange("password")}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
type="password"
|
||||
hint={[
|
||||
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<IconToolTip
|
||||
isHtml
|
||||
text={`\
|
||||
<div class="tooltip-text">\
|
||||
<p>This password is temporary.</p>\
|
||||
<p> This user will be asked to set a new password after logging in to the Fleet UI.</p>\
|
||||
<p>This user will not be asked to set a new password after logging in to fleetctl or the Fleet API.</p>\
|
||||
</div>\
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isBasicTier && (
|
||||
<div className={`${baseClass}__selected-teams-container`}>
|
||||
<div className={`${baseClass}__team-radios`}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
.create-user-form {
|
||||
&__new-user-container {
|
||||
margin-top: $pad-large;
|
||||
margin-bottom: $pad-large;
|
||||
}
|
||||
|
||||
&__sso-input {
|
||||
margin-top: $pad-large;
|
||||
margin-bottom: $pad-large;
|
||||
|
|
@ -51,6 +56,15 @@
|
|||
|
||||
&__radio-input {
|
||||
margin-bottom: $pad-medium;
|
||||
|
||||
&.disabled {
|
||||
.radio__control {
|
||||
background-color: $ui-fleet-black-25;
|
||||
}
|
||||
.radio__label {
|
||||
color: $ui-fleet-black-25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-wrap {
|
||||
|
|
@ -66,9 +80,38 @@
|
|||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__password {
|
||||
width: 98%;
|
||||
float: left;
|
||||
padding: 0 $pad-medium 0 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.input-icon-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__details {
|
||||
float: right;
|
||||
width: 2%;
|
||||
|
||||
.icon-tooltip {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: $core-fleet-black;
|
||||
|
||||
&--brand {
|
||||
color: $core-vibrant-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sublabel {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
&__tooltip-text {
|
||||
width: 300px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { userStub, userTeamStub } from "test/stubs";
|
||||
import { IFormData } from "../components/UserForm/UserForm";
|
||||
import { IFormData, NewUserType } from "../components/UserForm/UserForm";
|
||||
import userManagementHelpers from "./userManagementHelpers";
|
||||
|
||||
describe("userManagementHelpers module", () => {
|
||||
|
|
@ -19,6 +19,7 @@ describe("userManagementHelpers module", () => {
|
|||
email: "newemail@test.com",
|
||||
sso_enabled: false,
|
||||
name: "Gnar Mike",
|
||||
newUserType: NewUserType.AdminCreated, // TODO revisit test
|
||||
global_role: "admin",
|
||||
teams: [updatedTeam, newTeam],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@ import Fleet from "fleet";
|
|||
import config from "redux/nodes/entities/users/config";
|
||||
import { formatErrorResponse } from "redux/nodes/entities/base/helpers";
|
||||
import { logoutUser, updateUserSuccess } from "redux/nodes/auth/actions";
|
||||
import { create } from "lodash";
|
||||
|
||||
const { actions } = config;
|
||||
|
||||
// Actions for admin to require password reset for a user
|
||||
export const REQUIRE_PASSWORD_RESET_SUCCESS = "REQUIRE_PASSWORD_RESET_SUCCESS";
|
||||
export const REQUIRE_PASSWORD_RESET_FAILURE = "REQUIRE_PASSWORD_RESET_FAILURE";
|
||||
export const CREATE_USER_WITHOUT_INVITE_SUCCESS =
|
||||
"CREATE_USER_WITHOUT_INVITE_SUCCESS";
|
||||
export const CREATE_USER_WITHOUT_INVITE_FAILURE =
|
||||
"CREATE_USER_WITHOUT_INVITE_FAILURE";
|
||||
|
||||
export const requirePasswordResetSuccess = (user) => {
|
||||
return {
|
||||
|
|
@ -24,6 +29,21 @@ export const requirePasswordResetFailure = (errors) => {
|
|||
};
|
||||
};
|
||||
|
||||
// TODO does below need user
|
||||
export const createUserWithoutInviteSuccess = () => {
|
||||
return {
|
||||
type: CREATE_USER_WITHOUT_INVITE_SUCCESS,
|
||||
payload: {},
|
||||
};
|
||||
};
|
||||
|
||||
export const createUserWithoutInviteFailure = (errors) => {
|
||||
return {
|
||||
type: CREATE_USER_WITHOUT_INVITE_FAILURE,
|
||||
payload: { errors },
|
||||
};
|
||||
};
|
||||
|
||||
export const changePassword = (
|
||||
user,
|
||||
{ new_password: newPassword, old_password: oldPassword }
|
||||
|
|
@ -77,6 +97,23 @@ export const confirmEmailChange = (user, token) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const createUserWithoutInvitation = (formData) => {
|
||||
return (dispatch) => {
|
||||
return Fleet.users
|
||||
.createUserWithoutInvitation(formData)
|
||||
.then((response) => {
|
||||
return dispatch(createUserWithoutInviteSuccess(response));
|
||||
})
|
||||
.catch((response) => {
|
||||
const errorsObject = formatErrorResponse(response);
|
||||
|
||||
dispatch(createUserWithoutInviteFailure(errorsObject));
|
||||
|
||||
throw errorsObject;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteSessions = (user) => {
|
||||
const { successAction, destroyFailure, destroySuccess } = actions;
|
||||
|
||||
|
|
@ -156,6 +193,7 @@ export default {
|
|||
...actions,
|
||||
changePassword,
|
||||
confirmEmailChange,
|
||||
createUserWithoutInvitation,
|
||||
enableUser,
|
||||
requirePasswordReset,
|
||||
deleteSessions,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
REQUIRE_PASSWORD_RESET_FAILURE,
|
||||
REQUIRE_PASSWORD_RESET_SUCCESS,
|
||||
CREATE_USER_WITHOUT_INVITE_FAILURE,
|
||||
CREATE_USER_WITHOUT_INVITE_SUCCESS,
|
||||
} from "./actions";
|
||||
import config, { initialState } from "./config";
|
||||
|
||||
|
|
@ -22,6 +24,22 @@ export default (state = initialState, { type, payload }) => {
|
|||
loading: false,
|
||||
errors: payload.errors,
|
||||
};
|
||||
case CREATE_USER_WITHOUT_INVITE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
errors: {},
|
||||
loading: false,
|
||||
data: {
|
||||
...state.data,
|
||||
},
|
||||
};
|
||||
case CREATE_USER_WITHOUT_INVITE_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
errors: payload.errors,
|
||||
};
|
||||
|
||||
default:
|
||||
return config.reducer(state, { type, payload });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue