1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-18 13:48:58 +02:00

Merge branch 'main' into refactor/create-token

This commit is contained in:
Youssef Khedher 2022-01-18 13:14:27 +01:00 committed by GitHub
commit dd64f7110f
36 changed files with 1103 additions and 980 deletions

View File

@ -1,93 +0,0 @@
import { useState } from 'react';
import Dialogue from '../../../common/Dialogue';
import { IUserApiErrors } from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import IRole from '../../../../interfaces/role';
import AddUserForm from './AddUserForm/AddUserForm';
interface IAddUserProps {
showDialog: boolean;
closeDialog: () => void;
addUser: (data: any) => any;
validatePassword: () => boolean;
userLoading: boolean;
userApiErrors: IUserApiErrors;
roles: IRole[];
}
interface IAddUserFormData {
name: string;
email: string;
rootRole: number;
sendEmail: boolean;
}
const EDITOR_ROLE_ID = 2;
const initialData = { email: '', name: '', rootRole: EDITOR_ROLE_ID, sendEmail: true };
const AddUser = ({
showDialog,
closeDialog,
userLoading,
addUser,
userApiErrors,
roles,
}: IAddUserProps) => {
const [data, setData] = useState<IAddUserFormData>(initialData);
const [error, setError] = useState({});
const submit = async (e: Event) => {
e.preventDefault();
if (!data.email) {
setError({ general: 'You must specify the email address' });
return;
}
if (!data.rootRole) {
setError({ general: 'You must specify a role for the user' });
return;
}
await addUser(data);
setData(initialData);
setError({});
};
const onCancel = (e: Event) => {
e.preventDefault();
setData(initialData);
setError({});
closeDialog();
};
const formId = 'add-user-dialog-form';
return (
<Dialogue
onClick={e => {
submit(e);
}}
formId={formId}
open={showDialog}
onClose={onCancel}
primaryButtonText="Add user"
secondaryButtonText="Cancel"
title="Add team member"
fullWidth
>
<AddUserForm
formId={formId}
userApiErrors={userApiErrors}
data={data}
setData={setData}
roles={roles}
submit={submit}
error={error}
userLoading={userLoading}
/>
</Dialogue>
);
};
export default AddUser;

View File

@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
DialogContent,
FormControl,
FormControlLabel,
Radio,
RadioGroup,
Switch,
TextField,
Typography,
} from '@material-ui/core';
import { trim } from '../../../../common/util';
import { useCommonStyles } from '../../../../../common.styles';
import ConditionallyRender from '../../../../common/ConditionallyRender';
import { useStyles } from './AddUserForm.styles';
import useLoading from '../../../../../hooks/useLoading';
import {
ADD_USER_ERROR,
UPDATE_USER_ERROR,
} from '../../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import { Alert } from '@material-ui/lab';
function AddUserForm({
submit,
data,
error,
setData,
roles,
userLoading,
userApiErrors,
formId,
}) {
const ref = useLoading(userLoading);
const commonStyles = useCommonStyles();
const styles = useStyles();
const updateField = e => {
setData({
...data,
[e.target.name]: e.target.value,
});
};
const updateFieldWithTrim = e => {
setData({
...data,
[e.target.name]: trim(e.target.value),
});
};
const toggleBooleanField = e => {
setData({
...data,
[e.target.name]: !data[e.target.name],
});
};
const updateNumberField = e => {
setData({
...data,
[e.target.name]: +e.target.value,
});
};
const sortRoles = (a, b) => {
if (b.name[0] < a.name[0]) {
return 1;
} else if (a.name[0] < b.name[0]) {
return -1;
}
return 0;
};
const apiError =
userApiErrors[ADD_USER_ERROR] || userApiErrors[UPDATE_USER_ERROR];
return (
<div ref={ref}>
<form id={formId} onSubmit={submit}>
<DialogContent>
<ConditionallyRender
condition={apiError}
show={
<Alert
className={styles.errorAlert}
severity="error"
data-loading
>
{apiError}
</Alert>
}
/>
<div
className={classnames(
commonStyles.contentSpacingY,
commonStyles.flexColumn,
styles.userInfoContainer
)}
>
<Typography variant="subtitle1" data-loading>
Who is your team member?
</Typography>
<ConditionallyRender
condition={error.general}
show={
<p data-loading style={{ color: 'red' }}>
{error.general}
</p>
}
/>
<TextField
autoFocus
label="Full name"
data-loading
name="name"
value={data.name || ''}
error={error.name !== undefined}
helperText={error.name}
type="name"
variant="outlined"
size="small"
onChange={updateField}
/>
<TextField
label="Email"
data-loading
name="email"
required
value={data.email || ''}
error={error.email !== undefined}
helperText={error.email}
variant="outlined"
size="small"
type="email"
onChange={updateFieldWithTrim}
/>
<br />
<br />
</div>
<FormControl>
<Typography
variant="subtitle1"
className={styles.roleSubtitle}
data-loading
>
What is your team member allowed to do?
</Typography>
<RadioGroup
name="rootRole"
value={data.rootRole || ''}
onChange={updateNumberField}
data-loading
>
{roles.sort(sortRoles).map(role => (
<FormControlLabel
key={`role-${role.id}`}
labelPlacement="end"
className={styles.roleBox}
label={
<div>
<strong>{role.name}</strong>
<Typography variant="body2">
{role.description}
</Typography>
</div>
}
control={
<Radio
checked={role.id === data.rootRole}
className={styles.roleRadio}
/>
}
value={role.id}
/>
))}
</RadioGroup>
</FormControl>
<br />
<br />
<div className={commonStyles.flexRow}>
<FormControl>
<Typography
variant="subtitle1"
className={styles.roleSubtitle}
data-loading
>
Should we send an email to your new team member
</Typography>
<div className={commonStyles.flexRow}>
<Switch
name="sendEmail"
onChange={toggleBooleanField}
checked={data.sendEmail}
/>
<Typography>
{data.sendEmail ? 'Yes' : 'No'}
</Typography>
</div>
</FormControl>
</div>
</DialogContent>
</form>
</div>
);
}
AddUserForm.propTypes = {
data: PropTypes.object.isRequired,
error: PropTypes.object.isRequired,
submit: PropTypes.func.isRequired,
setData: PropTypes.func.isRequired,
roles: PropTypes.array.isRequired,
};
export default AddUserForm;

View File

@ -1,21 +0,0 @@
import { makeStyles } from '@material-ui/styles';
export const useStyles = makeStyles(theme => ({
roleBox: {
margin: '3px 0',
border: '1px solid #EFEFEF',
padding: '1rem',
},
userInfoContainer: {
margin: '-20px 0',
},
roleRadio: {
marginRight: '15px',
},
roleSubtitle: {
marginBottom: '0.5rem',
},
errorAlert: {
marginBottom: '1rem',
},
}));

View File

@ -0,0 +1,119 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory } from 'react-router-dom';
import UserForm from '../UserForm/UserForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import useAddUserForm from '../hooks/useAddUserForm';
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
import { useState } from 'react';
import { scrollToTop } from '../../../common/util';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
const CreateUser = () => {
/* @ts-ignore */
const { setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();
const {
name,
setName,
email,
setEmail,
sendEmail,
setSendEmail,
rootRole,
setRootRole,
getAddUserPayload,
validateName,
validateEmail,
errors,
clearErrors,
} = useAddUserForm();
const [showConfirm, setShowConfirm] = useState(false);
const [inviteLink, setInviteLink] = useState('');
const { addUser, userLoading: loading } = useAdminUsersApi();
const handleSubmit = async (e: Event) => {
e.preventDefault();
clearErrors();
const validName = validateName();
const validEmail = validateEmail();
if (validName && validEmail) {
const payload = getAddUserPayload();
try {
await addUser(payload)
.then(res => res.json())
.then(user => {
scrollToTop();
setInviteLink(user.inviteLink);
setShowConfirm(true);
});
} catch (e: any) {
setToastApiError(e.toString());
}
}
};
const closeConfirm = () => {
setShowConfirm(false);
history.push('/admin/user-admin');
};
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/api/admin/user-admin' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getAddUserPayload(), undefined, 2)}'`;
};
const handleCancel = () => {
history.goBack();
};
return (
<FormTemplate
loading={loading}
title="Create Unleash user"
description="In order to get access to Unleash needs to have an Unleash root role as Admin, Editor or Viewer.
You can also add the user to projects as member or owner in the specific projects."
documentationLink="https://docs.getunleash.io/user_guide/user-management"
formatApiCode={formatApiCode}
>
<UserForm
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
name={name}
setName={setName}
email={email}
setEmail={setEmail}
sendEmail={sendEmail}
setSendEmail={setSendEmail}
rootRole={rootRole}
setRootRole={setRootRole}
clearErrors={clearErrors}
>
<PermissionButton
onClick={handleSubmit}
permission={ADMIN}
type="submit"
>
Create user
</PermissionButton>
</UserForm>
<ConfirmUserAdded
open={showConfirm}
closeConfirm={closeConfirm}
emailSent={sendEmail}
inviteLink={inviteLink}
/>
</FormTemplate>
);
};
export default CreateUser;

View File

@ -0,0 +1,114 @@
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
import { useHistory, useParams } from 'react-router-dom';
import UserForm from '../UserForm/UserForm';
import useAddUserForm from '../hooks/useAddUserForm';
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../../hooks/useToast';
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import useUserInfo from '../../../../hooks/api/getters/useUserInfo/useUserInfo';
import { scrollToTop } from '../../../common/util';
import { useEffect } from 'react';
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../../providers/AccessProvider/permissions';
import { EDIT } from '../../../../constants/misc';
const EditUser = () => {
useEffect(() => {
scrollToTop();
}, []);
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const { id } = useParams<{ id: string }>();
const { user, refetch } = useUserInfo(id);
const { updateUser, userLoading: loading } = useAdminUsersApi();
const history = useHistory();
const {
name,
setName,
email,
setEmail,
sendEmail,
setSendEmail,
rootRole,
setRootRole,
getAddUserPayload,
validateName,
errors,
clearErrors,
} = useAddUserForm(
user?.name,
user?.email,
user?.sendEmail,
user?.rootRole
);
const formatApiCode = () => {
return `curl --location --request PUT '${
uiConfig.unleashUrl
}/api/admin/user-admin/${id}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getAddUserPayload(), undefined, 2)}'`;
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
const payload = getAddUserPayload();
const validName = validateName();
if (validName) {
try {
await updateUser({ ...payload, id });
refetch();
history.push('/admin/users');
setToastData({
title: 'User information updated',
type: 'success',
});
} catch (e: any) {
setToastApiError(e.toString());
}
}
};
const handleCancel = () => {
history.goBack();
};
return (
<FormTemplate
loading={loading}
title="Edit user"
description="In order to get access to Unleash needs to have an Unleash root role as Admin, Editor or Viewer.
You can also add the user to projects as member or owner in the specific projects."
documentationLink="https://docs.getunleash.io/user_guide/user-management"
formatApiCode={formatApiCode}
>
<UserForm
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
name={name}
setName={setName}
email={email}
setEmail={setEmail}
sendEmail={sendEmail}
setSendEmail={setSendEmail}
rootRole={rootRole}
setRootRole={setRootRole}
clearErrors={clearErrors}
mode={EDIT}
>
<PermissionButton
onClick={handleSubmit}
permission={ADMIN}
type="submit"
>
Edit user
</PermissionButton>
</UserForm>
</FormTemplate>
);
};
export default EditUser;

View File

@ -0,0 +1,68 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
maxWidth: '400px',
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
input: { width: '100%', marginBottom: '1rem' },
label: {
minWidth: '300px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginRight: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
roleBox: {
margin: '3px 0',
border: '1px solid #EFEFEF',
padding: '1rem',
},
userInfoContainer: {
margin: '-20px 0',
},
roleRadio: {
marginRight: '15px',
},
roleSubtitle: {
margin: '0.5rem 0',
},
errorAlert: {
marginBottom: '1rem',
},
flexRow: {
display: 'flex',
alignItems: 'center',
},
}));

View File

@ -0,0 +1,161 @@
import Input from '../../../common/Input/Input';
import {
FormControlLabel,
Button,
RadioGroup,
FormControl,
Typography,
Radio,
Switch,
} from '@material-ui/core';
import { useStyles } from './UserForm.styles';
import React from 'react';
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import { EDIT } from '../../../../constants/misc';
interface IUserForm {
email: string;
name: string;
rootRole: number;
sendEmail: boolean;
setEmail: React.Dispatch<React.SetStateAction<string>>;
setName: React.Dispatch<React.SetStateAction<string>>;
setSendEmail: React.Dispatch<React.SetStateAction<boolean>>;
setRootRole: React.Dispatch<React.SetStateAction<number>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
clearErrors: () => void;
mode?: string;
}
const UserForm: React.FC<IUserForm> = ({
children,
email,
name,
rootRole,
sendEmail,
setEmail,
setName,
setSendEmail,
setRootRole,
handleSubmit,
handleCancel,
errors,
clearErrors,
mode,
}) => {
const styles = useStyles();
const { roles } = useUsers();
const sortRoles = (a, b) => {
if (b.name[0] < a.name[0]) {
return 1;
} else if (a.name[0] < b.name[0]) {
return -1;
}
return 0;
};
return (
<form onSubmit={handleSubmit} className={styles.form}>
<h3 className={styles.formHeader}>User information</h3>
<div className={styles.container}>
<p className={styles.inputDescription}>
Who is the new Unleash user?
</p>
<Input
className={styles.input}
label="Full name"
value={name}
onChange={e => setName(e.target.value)}
error={Boolean(errors.name)}
errorText={errors.name}
onFocus={() => clearErrors()}
/>
<Input
className={styles.input}
label="Email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
error={Boolean(errors.email)}
errorText={errors.email}
onFocus={() => clearErrors()}
/>
<FormControl>
<Typography
variant="subtitle1"
className={styles.roleSubtitle}
data-loading
>
What is your team member allowed to do?
</Typography>
<RadioGroup
name="rootRole"
value={rootRole || ''}
onChange={e => setRootRole(+e.target.value)}
data-loading
>
{roles.sort(sortRoles).map(role => (
<FormControlLabel
key={`role-${role.id}`}
labelPlacement="end"
className={styles.roleBox}
label={
<div>
<strong>{role.name}</strong>
<Typography variant="body2">
{role.description}
</Typography>
</div>
}
control={
<Radio
checked={role.id === rootRole}
className={styles.roleRadio}
/>
}
value={role.id}
/>
))}
</RadioGroup>
</FormControl>
<ConditionallyRender
condition={mode !== EDIT}
show={
<FormControl>
<Typography
variant="subtitle1"
className={styles.roleSubtitle}
data-loading
>
Should we send an email to your new team member
</Typography>
<div className={styles.flexRow}>
<Switch
name="sendEmail"
onChange={() => setSendEmail(!sendEmail)}
checked={sendEmail}
/>
<Typography>
{sendEmail ? 'Yes' : 'No'}
</Typography>
</div>
</FormControl>
}
/>
</div>
<div className={styles.buttonContainer}>
<Button onClick={handleCancel} className={styles.cancelButton}>
Cancel
</Button>
{children}
</div>
</form>
);
};
export default UserForm;

View File

@ -13,6 +13,7 @@ import { formatDateWithLocale } from '../../../../common/util';
import AccessContext from '../../../../../contexts/AccessContext'; import AccessContext from '../../../../../contexts/AccessContext';
import { IUser } from '../../../../../interfaces/user'; import { IUser } from '../../../../../interfaces/user';
import { useStyles } from './UserListItem.styles'; import { useStyles } from './UserListItem.styles';
import { useHistory } from 'react-router-dom';
interface IUserListItemProps { interface IUserListItemProps {
user: IUser; user: IUser;
@ -36,6 +37,7 @@ const UserListItem = ({
location, location,
}: IUserListItemProps) => { }: IUserListItemProps) => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const history = useHistory()
const styles = useStyles(); const styles = useStyles();
return ( return (
@ -78,7 +80,7 @@ const UserListItem = ({
data-loading data-loading
aria-label="Edit" aria-label="Edit"
title="Edit" title="Edit"
onClick={openUpdateDialog(user)} onClick={()=> history.push(`/admin/users/${user.id}/edit`)}
> >
<Edit /> <Edit />
</IconButton> </IconButton>

View File

@ -8,9 +8,7 @@ import {
TableHead, TableHead,
TableRow, TableRow,
} from '@material-ui/core'; } from '@material-ui/core';
import AddUser from '../AddUser/AddUser';
import ChangePassword from '../change-password-component'; import ChangePassword from '../change-password-component';
import UpdateUser from '../update-user-component';
import DelUser from '../del-user-component'; import DelUser from '../del-user-component';
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
import AccessContext from '../../../../contexts/AccessContext'; import AccessContext from '../../../../contexts/AccessContext';
@ -27,9 +25,7 @@ import PaginateUI from '../../../common/PaginateUI/PaginateUI';
function UsersList({ location, closeDialog, showDialog }) { function UsersList({ location, closeDialog, showDialog }) {
const { users, roles, refetch, loading } = useUsers(); const { users, roles, refetch, loading } = useUsers();
const { const {
addUser,
removeUser, removeUser,
updateUser,
changePassword, changePassword,
validatePassword, validatePassword,
userLoading, userLoading,
@ -42,7 +38,6 @@ function UsersList({ location, closeDialog, showDialog }) {
const [emailSent, setEmailSent] = useState(false); const [emailSent, setEmailSent] = useState(false);
const [inviteLink, setInviteLink] = useState(''); const [inviteLink, setInviteLink] = useState('');
const [delUser, setDelUser] = useState(); const [delUser, setDelUser] = useState();
const [updateDialog, setUpdateDialog] = useState({ open: false });
const ref = useLoading(loading); const ref = useLoading(loading);
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } = const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
usePagination(users, 50); usePagination(users, 50);
@ -66,28 +61,6 @@ function UsersList({ location, closeDialog, showDialog }) {
setPwDialog({ open: false }); setPwDialog({ open: false });
}; };
const openUpdateDialog = user => e => {
e.preventDefault();
setUpdateDialog({ open: true, user });
};
const closeUpdateDialog = () => {
setUpdateDialog({ open: false });
};
const onAddUser = data => {
addUser(data)
.then(res => res.json())
.then(user => {
setEmailSent(user.emailSent);
setInviteLink(user.inviteLink);
closeDialog();
refetch();
setShowConfirm(true);
})
.catch(handleCatch);
};
const onDeleteUser = () => { const onDeleteUser = () => {
removeUser(delUser) removeUser(delUser)
.then(() => { .then(() => {
@ -97,15 +70,6 @@ function UsersList({ location, closeDialog, showDialog }) {
.catch(handleCatch); .catch(handleCatch);
}; };
const onUpdateUser = data => {
updateUser(data)
.then(() => {
refetch();
closeUpdateDialog();
})
.catch(handleCatch);
};
const handleCatch = () => const handleCatch = () =>
console.log('An exception was thrown and handled.'); console.log('An exception was thrown and handled.');
@ -126,7 +90,6 @@ function UsersList({ location, closeDialog, showDialog }) {
<UserListItem <UserListItem
key={user.id} key={user.id}
user={user} user={user}
openUpdateDialog={openUpdateDialog}
openPwDialog={openPwDialog} openPwDialog={openPwDialog}
openDelDialog={openDelDialog} openDelDialog={openDelDialog}
location={location} location={location}
@ -140,7 +103,6 @@ function UsersList({ location, closeDialog, showDialog }) {
<UserListItem <UserListItem
key={user.id} key={user.id}
user={user} user={user}
openUpdateDialog={openUpdateDialog}
openPwDialog={openPwDialog} openPwDialog={openPwDialog}
openDelDialog={openDelDialog} openDelDialog={openDelDialog}
location={location} location={location}
@ -185,26 +147,6 @@ function UsersList({ location, closeDialog, showDialog }) {
inviteLink={inviteLink} inviteLink={inviteLink}
/> />
<AddUser
showDialog={showDialog}
closeDialog={closeDialog}
addUser={onAddUser}
userLoading={userLoading}
validatePassword={validatePassword}
userApiErrors={userApiErrors}
roles={roles}
/>
<UpdateUser
showDialog={updateDialog.open}
closeDialog={closeUpdateDialog}
updateUser={onUpdateUser}
userLoading={userLoading}
userApiErrors={userApiErrors}
user={updateDialog.user}
roles={roles}
/>
<ChangePassword <ChangePassword
showDialog={pwDialog.open} showDialog={pwDialog.open}
closeDialog={closePwDialog} closeDialog={closePwDialog}

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { Typography, Avatar } from '@material-ui/core'; import { TextField, Typography, Avatar } from '@material-ui/core';
import { trim } from '../../common/util'; import { trim } from '../../common/util';
import { modalStyles } from './util'; import { modalStyles } from './util';
import Dialogue from '../../common/Dialogue/Dialogue'; import Dialogue from '../../common/Dialogue/Dialogue';
@ -10,7 +10,6 @@ import { useCommonStyles } from '../../../common.styles';
import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher'; import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import ConditionallyRender from '../../common/ConditionallyRender'; import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import PasswordField from '../../common/PasswordField/PasswordField';
function ChangePassword({ function ChangePassword({
showDialog, showDialog,
@ -110,20 +109,26 @@ function ChangePassword({
/> />
<p style={{ color: 'red' }}>{error.general}</p> <p style={{ color: 'red' }}>{error.general}</p>
<PasswordField <TextField
label="New password" label="New password"
name="password" name="password"
type="password"
value={data.password} value={data.password}
helperText={error.password} helperText={error.password}
onChange={updateField} onChange={updateField}
variant="outlined"
size="small"
/> />
<PasswordField <TextField
label="Confirm password" label="Confirm password"
name="confirm" name="confirm"
type="password"
value={data.confirm} value={data.confirm}
error={error.confirm !== undefined} error={error.confirm !== undefined}
helperText={error.confirm} helperText={error.confirm}
onChange={updateField} onChange={updateField}
variant="outlined"
size="small"
/> />
<PasswordMatcher <PasswordMatcher
started={data.password && data.confirm} started={data.password && data.confirm}

View File

@ -0,0 +1,84 @@
import { useEffect, useState } from 'react';
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
const useProjectRoleForm = (
initialName = '',
initialEmail = '',
initialSendEmail = false,
initialRootRole = 1
) => {
const [name, setName] = useState(initialName);
const [email, setEmail] = useState(initialEmail);
const [sendEmail, setSendEmail] = useState(initialSendEmail);
const [rootRole, setRootRole] = useState(initialRootRole);
const [errors, setErrors] = useState({});
const { users } = useUsers();
useEffect(() => {
setName(initialName);
}, [initialName]);
useEffect(() => {
setEmail(initialEmail);
}, [initialEmail]);
useEffect(() => {
setSendEmail(initialSendEmail);
}, [initialSendEmail]);
useEffect(() => {
setRootRole(initialRootRole);
}, [initialRootRole]);
const getAddUserPayload = () => {
return {
name: name,
email: email,
sendEmail: sendEmail,
rootRole: rootRole,
};
};
const validateName = () => {
if (name.length === 0) {
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
return false;
}
if (email.length === 0) {
setErrors(prev => ({ ...prev, email: 'Email can not be empty.' }));
return false;
}
return true;
};
const validateEmail = () => {
if (users.some(user => user['email'] === email)) {
setErrors(prev => ({ ...prev, email: 'Email already exists' }));
return false;
}
return true;
};
const clearErrors = () => {
setErrors({});
};
return {
name,
setName,
email,
setEmail,
sendEmail,
setSendEmail,
rootRole,
setRootRole,
getAddUserPayload,
validateName,
validateEmail,
clearErrors,
errors,
};
};
export default useProjectRoleForm;

View File

@ -40,7 +40,9 @@ const UsersAdmin = ({ history }) => {
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={openDialog} onClick={() =>
history.push('/admin/create-user')
}
> >
Add new user Add new user
</Button> </Button>

View File

@ -1,84 +0,0 @@
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Dialogue from '../../common/Dialogue';
import UserForm from './AddUser/AddUserForm/AddUserForm';
function AddUser({
user,
showDialog,
closeDialog,
updateUser,
roles,
userApiErrors,
userLoading,
}) {
const [data, setData] = useState({});
const [error, setError] = useState({});
useEffect(() => {
setData({
...user,
});
}, [user]);
if (!user) {
return null;
}
const submit = async e => {
e.preventDefault();
try {
await updateUser(data);
setData({});
setError({});
} catch (error) {
setError({ general: 'Could not update user' });
}
};
const onCancel = e => {
e.preventDefault();
setData({});
setError({});
closeDialog();
};
const formId = 'update-user-dialog-form'
return (
<Dialogue
onClick={e => {
submit(e);
}}
formId={formId}
open={showDialog}
onClose={onCancel}
primaryButtonText="Update user"
secondaryButtonText="Cancel"
title="Update team member"
fullWidth
>
<UserForm
data={data}
setData={setData}
roles={roles}
submit={submit}
error={error}
userApiErrors={userApiErrors}
userLoading={userLoading}
formId={formId}
/>
</Dialogue>
);
}
AddUser.propTypes = {
showDialog: PropTypes.bool.isRequired,
closeDialog: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired,
user: PropTypes.object,
roles: PropTypes.array.isRequired,
};
export default AddUser;

View File

@ -1,31 +0,0 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
helperText: { marginBottom: '1rem' },
formHeader: {
fontWeight: 'bold',
fontSize: '1rem',
marginTop: '2rem',
},
radioGroup: {
flexDirection: 'row',
},
environmentDetailsContainer: {
display: 'flex',
flexDirection: 'column',
margin: '1rem 0',
},
submitButton: {
marginTop: '1rem',
width: '150px',
marginRight: '1rem',
},
btnContainer: {
display: 'flex',
justifyContent: 'center',
},
inputField: {
width: '100%',
marginTop: '1rem',
},
}));

View File

@ -1,193 +1,141 @@
import React, { useState } from 'react';
import { FormControl, Button } from '@material-ui/core';
import HeaderTitle from '../../common/HeaderTitle';
import PageContent from '../../common/PageContent';
import { useStyles } from './CreateEnvironment.styles';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import useEnvironmentForm from '../hooks/useEnvironmentForm';
import ConditionallyRender from '../../common/ConditionallyRender'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import CreateEnvironmentSuccess from './CreateEnvironmentSuccess/CreateEnvironmentSuccess';
import useLoading from '../../../hooks/useLoading';
import useToast from '../../../hooks/useToast'; import useToast from '../../../hooks/useToast';
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector'; import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import Input from '../../common/Input/Input'; import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments'; import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import { Button } from '@material-ui/core';
import ConditionallyRender from '../../common/ConditionallyRender';
import PageContent from '../../common/PageContent';
import HeaderTitle from '../../common/HeaderTitle';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
const NAME_EXISTS_ERROR = 'Error: Environment';
const CreateEnvironment = () => { const CreateEnvironment = () => {
const [type, setType] = useState('development'); /* @ts-ignore */
const [envName, setEnvName] = useState(''); const { setToastApiError, setToastData } = useToast();
const [nameError, setNameError] = useState(''); const { uiConfig } = useUiConfig();
const [createSuccess, setCreateSucceess] = useState(false);
const history = useHistory(); const history = useHistory();
const styles = useStyles();
const { validateEnvName, createEnvironment, loading } = useEnvironmentApi();
const { environments } = useEnvironments(); const { environments } = useEnvironments();
const ref = useLoading(loading);
const { setToastApiError } = useToast();
const { refetch } = useProjectRolePermissions();
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
setType(event.currentTarget.value);
};
const handleEnvNameChange = (e: React.FormEvent<HTMLInputElement>) => {
setEnvName(e.currentTarget.value);
};
const goBack = () => history.goBack();
const canCreateMoreEnvs = environments.length < 7; const canCreateMoreEnvs = environments.length < 7;
const { createEnvironment, loading } = useEnvironmentApi();
const { refetch } = useProjectRolePermissions();
const {
name,
setName,
type,
setType,
getEnvPayload,
validateEnvironmentName,
clearErrors,
errors,
} = useEnvironmentForm();
const validateEnvironmentName = async () => { const handleSubmit = async (e: Event) => {
if (envName.length === 0) {
setNameError('Environment Id can not be empty.');
return false;
}
try {
await validateEnvName(envName);
} catch (e) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setNameError('Name already exists');
}
return false;
}
return true;
};
const clearNameError = () => setNameError('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
clearErrors();
const validName = await validateEnvironmentName(); const validName = await validateEnvironmentName();
if (validName) { if (validName) {
const environment = { const payload = getEnvPayload();
name: envName,
type,
};
try { try {
await createEnvironment(environment); await createEnvironment(payload);
refetch(); refetch();
setCreateSucceess(true); setToastData({
} catch (e) { title: 'Environment created',
type: 'success',
confetti: true,
});
history.push('/environments');
} catch (e: any) {
setToastApiError(e.toString()); setToastApiError(e.toString());
} }
} }
}; };
const formatApiCode = () => {
return `curl --location --request POST '${
uiConfig.unleashUrl
}/api/admin/environments' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(getEnvPayload(), undefined, 2)}'`;
};
const handleCancel = () => {
history.goBack();
};
return ( return (
<PageContent headerContent={<HeaderTitle title="Create environment" />}> <ConditionallyRender
<ConditionallyRender condition={canCreateMoreEnvs}
condition={createSuccess} show={
show={<CreateEnvironmentSuccess name={envName} type={type} />} <FormTemplate
elseShow={ loading={loading}
<ConditionallyRender title="Create Environment"
condition={canCreateMoreEnvs} description="Environments allow you to manage your
show={ product lifecycle from local development
<div ref={ref}> through production. Your projects and
<p className={styles.helperText} data-loading> feature toggles are accessible in all your
Environments allow you to manage your environments, but they can take different
product lifecycle from local development configurations per environment. This means
through production. Your projects and that you can enable a feature toggle in a
feature toggles are accessible in all your development or test environment without
environments, but they can take different enabling the feature toggle in the
configurations per environment. This means production environment."
that you can enable a feature toggle in a documentationLink="https://docs.getunleash.io/user_guide/environments"
development or test environment without formatApiCode={formatApiCode}
enabling the feature toggle in the >
production environment. <EnvironmentForm
</p> errors={errors}
handleSubmit={handleSubmit}
<form onSubmit={handleSubmit}> handleCancel={handleCancel}
<FormControl component="fieldset"> validateEnvironmentName={validateEnvironmentName}
<h3 name={name}
className={styles.formHeader} type={type}
data-loading setName={setName}
> setType={setType}
Environment Id and name mode="Create"
</h3> clearErrors={clearErrors}
>
<div <PermissionButton
data-loading onClick={handleSubmit}
className={ permission={ADMIN}
styles.environmentDetailsContainer type="submit"
} >
> Create environment
<p> </PermissionButton>
Unique env name for SDK </EnvironmentForm>
configurations. </FormTemplate>
</p> }
<Input elseShow={
label="Environment Id" <>
onFocus={clearNameError} <PageContent
placeholder="A unique name for your environment" headerContent={
onBlur={validateEnvironmentName} <HeaderTitle title="Create environment" />
error={Boolean(nameError)}
errorText={nameError}
value={envName}
onChange={handleEnvNameChange}
className={styles.inputField}
/>
</div>
<EnvironmentTypeSelector
onChange={handleTypeChange}
value={type}
/>
</FormControl>
<div className={styles.btnContainer}>
<Button
className={styles.submitButton}
variant="contained"
color="primary"
type="submit"
data-loading
>
Submit
</Button>{' '}
<Button
className={styles.submitButton}
variant="outlined"
color="secondary"
onClick={goBack}
data-loading
>
Cancel
</Button>
</div>
</form>
</div>
} }
elseShow={ >
<> <Alert severity="error">
<Alert severity="error"> <p>
<p> Currently Unleash does not support more than 7
Currently Unleash does not support more environments. If you need more please reach out.
than 5 environments. If you need more </p>
please reach out. </Alert>
</p> <br />
</Alert> <Button
<br /> onClick={handleCancel}
<Button variant="contained"
onClick={goBack} color="primary"
variant="contained" >
color="primary" Go back
> </Button>
Go back </PageContent>
</Button> </>
</> }
} />
/>
}
/>
</PageContent>
); );
}; };

View File

@ -1,41 +0,0 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
subheader: {
fontSize: theme.fontSizes.subHeader,
fontWeight: 'normal',
marginTop: '2rem',
},
container: {
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
},
nextSteps: {
display: 'flex',
},
step: { maxWidth: '350px', margin: '0 1.5rem', position: 'relative' },
stepBadge: {
backgroundColor: theme.palette.primary.main,
width: '30px',
height: '30px',
borderRadius: '25px',
color: '#fff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontWeight: 'bold',
margin: '2rem auto',
},
stepParagraph: {
marginBottom: '1rem',
},
button: {
marginTop: '2.5rem',
minWidth: '150px',
},
link: {
color: theme.palette.primary.main,
},
}));

View File

@ -1,89 +0,0 @@
/* eslint-disable react/jsx-no-target-blank */
import { Button } from '@material-ui/core';
import { useHistory } from 'react-router-dom';
import CheckMarkBadge from '../../../common/CheckmarkBadge/CheckMarkBadge';
import { useStyles } from './CreateEnvironmentSuccess.styles';
import CreateEnvironmentSuccessCard from './CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
export interface ICreateEnvironmentSuccessProps {
name: string;
type: string;
}
const CreateEnvironmentSuccess = ({
name,
type,
}: ICreateEnvironmentSuccessProps) => {
const history = useHistory();
const styles = useStyles();
const navigateToEnvironmentList = () => {
history.push('/environments');
};
return (
<div className={styles.container}>
<CheckMarkBadge />
<h2 className={styles.subheader}>Environment created</h2>
<CreateEnvironmentSuccessCard
name={name}
type={type}
/>
<h2 className={styles.subheader}>Next steps</h2>
<div className={styles.nextSteps}>
<div className={styles.step}>
<div>
<div className={styles.stepBadge}>1</div>
<h3 className={styles.subheader}>
Update SDK version and provide the environment id to
the SDK
</h3>
<p className={styles.stepParagraph}>
By providing the environment id in the SDK the SDK
will only retrieve activation strategies for
specified environment
</p>
<a
href="https://docs.getunleash.io/user_guide/environments"
target="_blank"
className={styles.link}
>
Learn more
</a>
</div>
</div>
<div className={styles.step}>
<div>
<div className={styles.stepBadge}>2</div>
<h3 className={styles.subheader}>
Add environment specific activation strategies
</h3>
<p className={styles.stepParagraph}>
You can now select this environment when you are
adding new activation strategies on feature toggles.
</p>
<a
href="https://docs.getunleash.io/user_guide/environments"
target="_blank"
className={styles.link}
>
Learn more
</a>
</div>
</div>
</div>
<Button
variant="contained"
color="primary"
className={styles.button}
onClick={navigateToEnvironmentList}
>
Got it!
</Button>
</div>
);
};
export default CreateEnvironmentSuccess;

View File

@ -1,54 +0,0 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
minWidth: '300px',
position: 'absolute',
right: '80px',
bottom: '-475px',
zIndex: 9999,
opacity: 0,
transform: 'translateY(100px)',
},
inputField: {
width: '100%',
},
header: {
fontSize: theme.fontSizes.subHeader,
fontWeight: 'normal',
borderBottom: `1px solid ${theme.palette.grey[300]}`,
padding: '1rem',
},
body: { padding: '1rem' },
subheader: {
display: 'flex',
alignItems: 'center',
fontSize: theme.fontSizes.bodySize,
fontWeight: 'normal',
},
icon: {
marginRight: '0.5rem',
fill: theme.palette.grey[600],
},
formHeader: {
fontSize: theme.fontSizes.bodySize,
},
buttonGroup: {
marginTop: '2rem',
display: 'flex',
justifyContent: 'space-between',
},
editEnvButton: {
width: '150px',
},
fadeInBottomEnter: {
transform: 'translateY(0)',
opacity: '1',
transition: 'transform 0.4s ease, opacity .4s ease',
},
fadeInBottomLeave: {
transform: 'translateY(100px)',
opacity: '0',
transition: 'transform 0.4s ease, opacity 0.4s ease',
},
}));

View File

@ -1,108 +1,99 @@
import { CloudCircle } from '@material-ui/icons'; import { useHistory, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
import { useStyles } from './EditEnvironment.styles';
import { IEnvironment } from '../../../interfaces/environments';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useLoading from '../../../hooks/useLoading'; import useEnvironment from '../../../hooks/api/getters/useEnvironment/useEnvironment';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import Dialogue from '../../common/Dialogue'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { ADMIN } from '../../providers/AccessProvider/permissions';
import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import useEnvironmentForm from '../hooks/useEnvironmentForm';
interface IEditEnvironmentProps { const EditEnvironment = () => {
env: IEnvironment; const { uiConfig } = useUiConfig();
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>; const { setToastData, setToastApiError } = useToast();
editEnvironment: boolean;
setToastData: React.Dispatch<React.SetStateAction<any>>;
}
const EditEnvironment = ({ const { id } = useParams<{ id: string }>();
env, const { environment } = useEnvironment(id);
setEditEnvironment, const { updateEnvironment } = useEnvironmentApi();
editEnvironment,
setToastData,
}: IEditEnvironmentProps) => {
const styles = useStyles();
const [type, setType] = useState(env.type);
const { updateEnvironment, loading } = useEnvironmentApi();
const { refetch } = useEnvironments();
const ref = useLoading(loading);
useEffect(() => { const history = useHistory();
setType(env.type); const { name, type, setName, setType, errors, clearErrors } =
}, [env.type]); useEnvironmentForm(environment.name, environment.type);
const { refetch } = useProjectRolePermissions();
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => { const editPayload = () => {
setType(event.currentTarget.value); return {
};
const isDisabled = () => {
return type === env.type;
};
const handleCancel = () => {
setEditEnvironment(false);
resetFields();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const updatedEnv = {
sortOrder: env.sortOrder,
type, type,
sortOrder: environment.sortOrder,
}; };
};
const formatApiCode = () => {
return `curl --location --request PUT '${
uiConfig.unleashUrl
}/api/admin/environments/update/${id}' \\
--header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\
--data-raw '${JSON.stringify(editPayload(), undefined, 2)}'`;
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
try { try {
await updateEnvironment(env.name, updatedEnv); await updateEnvironment(id, editPayload());
refetch();
history.push('/environments');
setToastData({ setToastData({
type: 'success', type: 'success',
show: true, title: 'Successfully updated environment.',
text: 'Successfully updated environment.',
}); });
resetFields(); } catch (e: any) {
refetch(); setToastApiError(e.toString());
} catch (e) {
setToastData({
show: true,
type: 'error',
text: e.toString(),
});
} finally {
setEditEnvironment(false);
} }
}; };
const resetFields = () => { const handleCancel = () => {
setType(env.type); history.goBack();
}; };
const formId = 'edit-environment-form';
return ( return (
<Dialogue <FormTemplate
open={editEnvironment}
title="Edit environment" title="Edit environment"
onClose={handleCancel} description="Environments allow you to manage your
onClick={handleSubmit} product lifecycle from local development
primaryButtonText="Save" through production. Your projects and
secondaryButtonText="Cancel" feature toggles are accessible in all your
disabledPrimaryButton={isDisabled()} environments, but they can take different
formId={formId} configurations per environment. This means
that you can enable a feature toggle in a
development or test environment without
enabling the feature toggle in the
production environment."
documentationLink="https://docs.getunleash.io/user_guide/environments"
formatApiCode={formatApiCode}
> >
<div className={styles.body} ref={ref}> <EnvironmentForm
<h3 className={styles.formHeader} data-loading> handleSubmit={handleSubmit}
Environment Id handleCancel={handleCancel}
</h3> name={name}
<h3 className={styles.subheader} data-loading> type={type}
<CloudCircle className={styles.icon} /> {env.name} setName={setName}
</h3> setType={setType}
<form id={formId}> mode="Edit"
<EnvironmentTypeSelector errors={errors}
onChange={handleTypeChange} clearErrors={clearErrors}
value={type} >
/> <PermissionButton
</form> onClick={handleSubmit}
</div> permission={ADMIN}
</Dialogue> type="submit"
>
Edit environment
</PermissionButton>
</EnvironmentForm>
</FormTemplate>
); );
}; };

View File

@ -0,0 +1,47 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
maxWidth: '440px',
},
form: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
input: { width: '100%', marginBottom: '1rem' },
label: {
minWidth: '30px',
[theme.breakpoints.down(600)]: {
minWidth: 'auto',
},
},
buttonContainer: {
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
},
cancelButton: {
marginRight: '1.5rem',
},
inputDescription: {
marginBottom: '0.5rem',
},
formHeader: {
fontWeight: 'normal',
marginTop: '0',
},
header: {
fontWeight: 'normal',
},
permissionErrorContainer: {
position: 'relative',
},
errorMessage: {
//@ts-ignore
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
top: '-8px',
},
}));

View File

@ -0,0 +1,74 @@
import { Button } from '@material-ui/core';
import { useStyles } from './EnvironmentForm.styles';
import React from 'react';
import Input from '../../common/Input/Input';
import EnvironmentTypeSelector from './EnvironmentTypeSelector/EnvironmentTypeSelector';
import { trim } from '../../common/util';
interface IEnvironmentForm {
name: string;
type: string;
setName: React.Dispatch<React.SetStateAction<string>>;
setType: React.Dispatch<React.SetStateAction<string>>;
validateEnvironmentName?: (e: any) => void;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
mode: string;
clearErrors: () => void;
}
const EnvironmentForm: React.FC<IEnvironmentForm> = ({
children,
handleSubmit,
handleCancel,
name,
type,
setName,
setType,
validateEnvironmentName,
errors,
mode,
clearErrors,
}) => {
const styles = useStyles();
return (
<form onSubmit={handleSubmit} className={styles.form}>
<h3 className={styles.formHeader}>Environment information</h3>
<div className={styles.container}>
<p className={styles.inputDescription}>
What is your environment name? (Can't be changed later)
</p>
<Input
className={styles.input}
label="Environment name"
value={name}
onChange={e => setName(trim(e.target.value))}
error={Boolean(errors.name)}
errorText={errors.name}
onFocus={() => clearErrors()}
onBlur={validateEnvironmentName}
disabled={mode === 'Edit'}
/>
<p className={styles.inputDescription}>
What type of environment do you want to create?
</p>
<EnvironmentTypeSelector
onChange={e => setType(e.currentTarget.value)}
value={type}
/>
</div>
<div className={styles.buttonContainer}>
<Button onClick={handleCancel} className={styles.cancelButton}>
Cancel
</Button>
{children}
</div>
</form>
);
};
export default EnvironmentForm;

View File

@ -6,6 +6,7 @@ export const useStyles = makeStyles(theme => ({
}, },
formHeader: { formHeader: {
fontWeight: 'bold', fontWeight: 'bold',
//@ts-ignore
fontSize: theme.fontSizes.bodySize, fontSize: theme.fontSizes.bodySize,
marginTop: '1.5rem', marginTop: '1.5rem',
marginBottom: '0.5rem', marginBottom: '0.5rem',

View File

@ -18,10 +18,6 @@ const EnvironmentTypeSelector = ({
const styles = useStyles(); const styles = useStyles();
return ( return (
<FormControl component="fieldset"> <FormControl component="fieldset">
<h3 className={styles.formHeader} data-loading>
Environment Type
</h3>
<RadioGroup <RadioGroup
data-loading data-loading
value={value} value={value}

View File

@ -1,12 +1,13 @@
import { CloudCircle } from '@material-ui/icons'; import { CloudCircle } from '@material-ui/icons';
import StringTruncator from '../../../../common/StringTruncator/StringTruncator'; import StringTruncator from '../../../common/StringTruncator/StringTruncator';
import { ICreateEnvironmentSuccessProps } from '../CreateEnvironmentSuccess'; import { useStyles } from './EnvironmentCard.styles';
import { useStyles } from './CreateEnvironmentSuccessCard.styles';
const CreateEnvironmentSuccessCard = ({ interface IEnvironmentProps {
name, name: string;
type, type: string;
}: ICreateEnvironmentSuccessProps) => { }
const EnvironmentCard = ({ name, type }: IEnvironmentProps) => {
const styles = useStyles(); const styles = useStyles();
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -30,4 +31,4 @@ const CreateEnvironmentSuccessCard = ({
); );
}; };
export default CreateEnvironmentSuccessCard; export default EnvironmentCard;

View File

@ -3,7 +3,7 @@ import React from 'react';
import { IEnvironment } from '../../../../interfaces/environments'; import { IEnvironment } from '../../../../interfaces/environments';
import Dialogue from '../../../common/Dialogue'; import Dialogue from '../../../common/Dialogue';
import Input from '../../../common/Input/Input'; import Input from '../../../common/Input/Input';
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard'; import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
import { useStyles } from './EnvironmentDeleteConfirm.styles'; import { useStyles } from './EnvironmentDeleteConfirm.styles';
interface IEnviromentDeleteConfirmProps { interface IEnviromentDeleteConfirmProps {
@ -52,7 +52,7 @@ const EnvironmentDeleteConfirm = ({
strategies that are active in this environment across all strategies that are active in this environment across all
feature toggles. feature toggles.
</Alert> </Alert>
<CreateEnvironmentSuccessCard name={env?.name} type={env?.type} /> <EnvironmentCard name={env?.name} type={env?.type} />
<p className={styles.deleteParagraph}> <p className={styles.deleteParagraph}>
In order to delete this environment, please enter the id of the In order to delete this environment, please enter the id of the

View File

@ -17,7 +17,6 @@ import useToast from '../../../hooks/useToast';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem'; import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
import { mutate } from 'swr'; import { mutate } from 'swr';
import EditEnvironment from '../EditEnvironment/EditEnvironment';
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm'; import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
@ -31,7 +30,6 @@ const EnvironmentList = () => {
protected: false, protected: false,
}; };
const { environments, refetch } = useEnvironments(); const { environments, refetch } = useEnvironments();
const [editEnvironment, setEditEnvironment] = useState(false);
const { refetch: refetchProjectRolePermissions } = const { refetch: refetchProjectRolePermissions } =
useProjectRolePermissions(); useProjectRolePermissions();
@ -151,7 +149,6 @@ const EnvironmentList = () => {
<EnvironmentListItem <EnvironmentListItem
key={env.name} key={env.name}
env={env} env={env}
setEditEnvironment={setEditEnvironment}
setDeldialogue={setDeldialogue} setDeldialogue={setDeldialogue}
setSelectedEnv={setSelectedEnv} setSelectedEnv={setSelectedEnv}
setToggleDialog={setToggleDialog} setToggleDialog={setToggleDialog}
@ -195,13 +192,6 @@ const EnvironmentList = () => {
confirmName={confirmName} confirmName={confirmName}
setConfirmName={setConfirmName} setConfirmName={setConfirmName}
/> />
<EditEnvironment
env={selectedEnv}
setEditEnvironment={setEditEnvironment}
editEnvironment={editEnvironment}
setToastData={setToastData}
/>
<EnvironmentToggleConfirm <EnvironmentToggleConfirm
env={selectedEnv} env={selectedEnv}
open={toggleDialog} open={toggleDialog}

View File

@ -25,12 +25,12 @@ import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
import { XYCoord } from 'dnd-core'; import { XYCoord } from 'dnd-core';
import DisabledIndicator from '../../../common/DisabledIndicator/DisabledIndicator'; import DisabledIndicator from '../../../common/DisabledIndicator/DisabledIndicator';
import StringTruncator from '../../../common/StringTruncator/StringTruncator'; import StringTruncator from '../../../common/StringTruncator/StringTruncator';
import { useHistory } from 'react-router-dom';
interface IEnvironmentListItemProps { interface IEnvironmentListItemProps {
env: IEnvironment; env: IEnvironment;
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>; setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>; setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>;
setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>; setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>;
index: number; index: number;
moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[]; moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[];
@ -51,8 +51,8 @@ const EnvironmentListItem = ({
moveListItem, moveListItem,
moveListItemApi, moveListItemApi,
setToggleDialog, setToggleDialog,
setEditEnvironment,
}: IEnvironmentListItemProps) => { }: IEnvironmentListItemProps) => {
const history = useHistory();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const ACCEPT_TYPE = 'LIST_ITEM'; const ACCEPT_TYPE = 'LIST_ITEM';
const [{ isDragging }, drag] = useDrag({ const [{ isDragging }, drag] = useDrag({
@ -178,8 +178,7 @@ const EnvironmentListItem = ({
aria-label="update" aria-label="update"
disabled={env.protected} disabled={env.protected}
onClick={() => { onClick={() => {
setSelectedEnv(env); history.push(`/environments/${env.name}`);
setEditEnvironment(prev => !prev);
}} }}
> >
<Edit /> <Edit />

View File

@ -4,7 +4,7 @@ import React from 'react';
import { IEnvironment } from '../../../../interfaces/environments'; import { IEnvironment } from '../../../../interfaces/environments';
import ConditionallyRender from '../../../common/ConditionallyRender'; import ConditionallyRender from '../../../common/ConditionallyRender';
import Dialogue from '../../../common/Dialogue'; import Dialogue from '../../../common/Dialogue';
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard'; import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
interface IEnvironmentToggleConfirmProps { interface IEnvironmentToggleConfirmProps {
env: IEnvironment; env: IEnvironment;
@ -52,10 +52,7 @@ const EnvironmentToggleConfirm = ({
} }
/> />
<CreateEnvironmentSuccessCard <EnvironmentCard name={env?.name} type={env?.type} />
name={env?.name}
type={env?.type}
/>
</Dialogue> </Dialogue>
); );
}; };

View File

@ -0,0 +1,69 @@
import { useEffect, useState } from 'react';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
const useEnvironmentForm = (
initialName = '',
initialType = 'development'
) => {
const NAME_EXISTS_ERROR = 'Error: Environment';
const [name, setName] = useState(initialName);
const [type, setType] = useState(initialType);
const [errors, setErrors] = useState({});
useEffect(() => {
setName(initialName);
}, [initialName]);
useEffect(() => {
setType(initialType);
}, [initialType]);
const { validateEnvName } = useEnvironmentApi();
const getEnvPayload = () => {
return {
name,
type,
};
};
const validateEnvironmentName = async () => {
if (name.length === 0) {
setErrors(prev => ({
...prev,
name: 'Environment name can not be empty',
}));
return false;
}
try {
await validateEnvName(name);
} catch (e: any) {
if (e.toString().includes(NAME_EXISTS_ERROR)) {
setErrors(prev => ({
...prev,
name: 'Name already exists',
}));
}
return false;
}
return true;
};
const clearErrors = () => {
setErrors({});
};
return {
name,
setName,
type,
setType,
getEnvPayload,
validateEnvironmentName,
clearErrors,
errors,
};
};
export default useEnvironmentForm;

View File

@ -214,6 +214,14 @@ Array [
"title": "Environments", "title": "Environments",
"type": "protected", "type": "protected",
}, },
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"path": "/environments/:id",
"title": "Edit",
"type": "protected",
},
Object { Object {
"component": [Function], "component": [Function],
"flag": "EEA", "flag": "EEA",
@ -380,6 +388,15 @@ Array [
"title": "Users", "title": "Users",
"type": "protected", "type": "protected",
}, },
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"parent": "/admin",
"path": "/admin/create-user",
"title": "Users",
"type": "protected",
},
Object { Object {
"component": Object { "component": Object {
"$$typeof": Symbol(react.memo), "$$typeof": Symbol(react.memo),

View File

@ -37,13 +37,17 @@ import Project from '../project/Project/Project';
import RedirectFeatureViewPage from '../../page/features/redirect'; import RedirectFeatureViewPage from '../../page/features/redirect';
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive'; import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList'; import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import FeatureView2 from '../feature/FeatureView2/FeatureView2'; import FeatureView2 from '../feature/FeatureView2/FeatureView2';
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate'; import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles'; import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles';
import CreateProjectRole from '../admin/project-roles/CreateProjectRole/CreateProjectRole'; import CreateProjectRole from '../admin/project-roles/CreateProjectRole/CreateProjectRole';
import EditProjectRole from '../admin/project-roles/EditProjectRole/EditProjectRole'; import EditProjectRole from '../admin/project-roles/EditProjectRole/EditProjectRole';
import CreateUser from '../admin/users/CreateUser/CreateUser';
import EditUser from '../admin/users/EditUser/EditUser';
import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken'; import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken';
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
export const routes = [ export const routes = [
// Project // Project
@ -255,6 +259,14 @@ export const routes = [
layout: 'main', layout: 'main',
menu: {}, menu: {},
}, },
{
path: '/environments/:id',
title: 'Edit',
component: EditEnvironment,
type: 'protected',
layout: 'main',
menu: {},
},
{ {
path: '/environments', path: '/environments',
title: 'Environments', title: 'Environments',
@ -406,6 +418,15 @@ export const routes = [
menu: {}, menu: {},
flag: RE, flag: RE,
}, },
{
path: '/admin/users/:id/edit',
title: 'Edit',
component: EditUser,
type: 'protected',
layout: 'main',
menu: {},
hidden: true,
},
{ {
path: '/admin/api', path: '/admin/api',
parent: '/admin', parent: '/admin',
@ -424,6 +445,15 @@ export const routes = [
layout: 'main', layout: 'main',
menu: { adminSettings: true }, menu: { adminSettings: true },
}, },
{
path: '/admin/create-user',
parent: '/admin',
title: 'Users',
component: CreateUser,
type: 'protected',
layout: 'main',
menu: {},
},
{ {
path: '/admin/auth', path: '/admin/auth',
parent: '/admin', parent: '/admin',

View File

@ -0,0 +1,2 @@
export const EDIT = 'Edit';
export const CREATE = 'Create';

View File

@ -0,0 +1,10 @@
import { IEnvironment } from '../../../../interfaces/environments';
export const defaultEnvironment: IEnvironment = {
name: '',
type: '',
createdAt: '',
sortOrder: 0,
enabled: false,
protected: false
};

View File

@ -0,0 +1,49 @@
import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import { IEnvironment } from '../../../../interfaces/environments';
import handleErrorResponses from '../httpErrorResponseHandler';
import { defaultEnvironment } from './defaultEnvironment';
const useEnvironment = (
id: string,
options: SWRConfiguration = {}
) => {
const fetcher = async () => {
const path = formatApiPath(
`api/admin/environments/${id}`
);
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('Environment data'))
.then(res => res.json());
};
const FEATURE_CACHE_KEY = `api/admin/environments/${id}`;
const { data, error } = useSWR<IEnvironment>(FEATURE_CACHE_KEY, fetcher, {
...options,
});
const [loading, setLoading] = useState(!error && !data);
const refetch = () => {
mutate(FEATURE_CACHE_KEY);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return {
environment: data || defaultEnvironment,
error,
loading,
refetch,
FEATURE_CACHE_KEY,
};
};
export default useEnvironment;

View File

@ -0,0 +1,35 @@
import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
const useUserInfo = (id: string, options: SWRConfiguration = {}) => {
const fetcher = () => {
const path = formatApiPath(`api/admin/user-admin/${id}`);
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('Users'))
.then(res => res.json());
};
const { data, error } = useSWR(`api/admin/user-admin/${id}`, fetcher, options);
const [loading, setLoading] = useState(!error && !data);
const refetch = () => {
mutate(`api/admin/user-admin/${id}`);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return {
user: data || {},
error,
loading,
refetch,
};
};
export default useUserInfo;