1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-13 13:48:59 +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 { IUser } from '../../../../../interfaces/user';
import { useStyles } from './UserListItem.styles';
import { useHistory } from 'react-router-dom';
interface IUserListItemProps {
user: IUser;
@ -36,6 +37,7 @@ const UserListItem = ({
location,
}: IUserListItemProps) => {
const { hasAccess } = useContext(AccessContext);
const history = useHistory()
const styles = useStyles();
return (
@ -78,7 +80,7 @@ const UserListItem = ({
data-loading
aria-label="Edit"
title="Edit"
onClick={openUpdateDialog(user)}
onClick={()=> history.push(`/admin/users/${user.id}/edit`)}
>
<Edit />
</IconButton>

View File

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

View File

@ -1,7 +1,7 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
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 { modalStyles } from './util';
import Dialogue from '../../common/Dialogue/Dialogue';
@ -10,7 +10,6 @@ import { useCommonStyles } from '../../../common.styles';
import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
import ConditionallyRender from '../../common/ConditionallyRender';
import { Alert } from '@material-ui/lab';
import PasswordField from '../../common/PasswordField/PasswordField';
function ChangePassword({
showDialog,
@ -110,20 +109,26 @@ function ChangePassword({
/>
<p style={{ color: 'red' }}>{error.general}</p>
<PasswordField
<TextField
label="New password"
name="password"
type="password"
value={data.password}
helperText={error.password}
onChange={updateField}
variant="outlined"
size="small"
/>
<PasswordField
<TextField
label="Confirm password"
name="confirm"
type="password"
value={data.confirm}
error={error.confirm !== undefined}
helperText={error.confirm}
onChange={updateField}
variant="outlined"
size="small"
/>
<PasswordMatcher
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
variant="contained"
color="primary"
onClick={openDialog}
onClick={() =>
history.push('/admin/create-user')
}
>
Add new user
</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 useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import ConditionallyRender from '../../common/ConditionallyRender';
import CreateEnvironmentSuccess from './CreateEnvironmentSuccess/CreateEnvironmentSuccess';
import useLoading from '../../../hooks/useLoading';
import useEnvironmentForm from '../hooks/useEnvironmentForm';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useToast from '../../../hooks/useToast';
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
import Input from '../../common/Input/Input';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
import FormTemplate from '../../common/FormTemplate/FormTemplate';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
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';
const NAME_EXISTS_ERROR = 'Error: Environment';
const CreateEnvironment = () => {
const [type, setType] = useState('development');
const [envName, setEnvName] = useState('');
const [nameError, setNameError] = useState('');
const [createSuccess, setCreateSucceess] = useState(false);
/* @ts-ignore */
const { setToastApiError, setToastData } = useToast();
const { uiConfig } = useUiConfig();
const history = useHistory();
const styles = useStyles();
const { validateEnvName, createEnvironment, loading } = useEnvironmentApi();
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 { createEnvironment, loading } = useEnvironmentApi();
const { refetch } = useProjectRolePermissions();
const {
name,
setName,
type,
setType,
getEnvPayload,
validateEnvironmentName,
clearErrors,
errors,
} = useEnvironmentForm();
const validateEnvironmentName = async () => {
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) => {
const handleSubmit = async (e: Event) => {
e.preventDefault();
clearErrors();
const validName = await validateEnvironmentName();
if (validName) {
const environment = {
name: envName,
type,
};
const payload = getEnvPayload();
try {
await createEnvironment(environment);
await createEnvironment(payload);
refetch();
setCreateSucceess(true);
} catch (e) {
setToastData({
title: 'Environment created',
type: 'success',
confetti: true,
});
history.push('/environments');
} catch (e: any) {
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 (
<PageContent headerContent={<HeaderTitle title="Create environment" />}>
<ConditionallyRender
condition={createSuccess}
show={<CreateEnvironmentSuccess name={envName} type={type} />}
elseShow={
<ConditionallyRender
condition={canCreateMoreEnvs}
show={
<div ref={ref}>
<p className={styles.helperText} data-loading>
Environments allow you to manage your
product lifecycle from local development
through production. Your projects and
feature toggles are accessible in all your
environments, but they can take different
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.
</p>
<form onSubmit={handleSubmit}>
<FormControl component="fieldset">
<h3
className={styles.formHeader}
data-loading
>
Environment Id and name
</h3>
<div
data-loading
className={
styles.environmentDetailsContainer
}
>
<p>
Unique env name for SDK
configurations.
</p>
<Input
label="Environment Id"
onFocus={clearNameError}
placeholder="A unique name for your environment"
onBlur={validateEnvironmentName}
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>
<ConditionallyRender
condition={canCreateMoreEnvs}
show={
<FormTemplate
loading={loading}
title="Create Environment"
description="Environments allow you to manage your
product lifecycle from local development
through production. Your projects and
feature toggles are accessible in all your
environments, but they can take different
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}
>
<EnvironmentForm
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
validateEnvironmentName={validateEnvironmentName}
name={name}
type={type}
setName={setName}
setType={setType}
mode="Create"
clearErrors={clearErrors}
>
<PermissionButton
onClick={handleSubmit}
permission={ADMIN}
type="submit"
>
Create environment
</PermissionButton>
</EnvironmentForm>
</FormTemplate>
}
elseShow={
<>
<PageContent
headerContent={
<HeaderTitle title="Create environment" />
}
elseShow={
<>
<Alert severity="error">
<p>
Currently Unleash does not support more
than 5 environments. If you need more
please reach out.
</p>
</Alert>
<br />
<Button
onClick={goBack}
variant="contained"
color="primary"
>
Go back
</Button>
</>
}
/>
}
/>
</PageContent>
>
<Alert severity="error">
<p>
Currently Unleash does not support more than 7
environments. If you need more please reach out.
</p>
</Alert>
<br />
<Button
onClick={handleCancel}
variant="contained"
color="primary"
>
Go back
</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 { useEffect, useState } from 'react';
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
import { useStyles } from './EditEnvironment.styles';
import { IEnvironment } from '../../../interfaces/environments';
import { useHistory, useParams } from 'react-router-dom';
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useLoading from '../../../hooks/useLoading';
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
import Dialogue from '../../common/Dialogue';
import useEnvironment from '../../../hooks/api/getters/useEnvironment/useEnvironment';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
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 {
env: IEnvironment;
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>;
editEnvironment: boolean;
setToastData: React.Dispatch<React.SetStateAction<any>>;
}
const EditEnvironment = () => {
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const EditEnvironment = ({
env,
setEditEnvironment,
editEnvironment,
setToastData,
}: IEditEnvironmentProps) => {
const styles = useStyles();
const [type, setType] = useState(env.type);
const { updateEnvironment, loading } = useEnvironmentApi();
const { refetch } = useEnvironments();
const ref = useLoading(loading);
const { id } = useParams<{ id: string }>();
const { environment } = useEnvironment(id);
const { updateEnvironment } = useEnvironmentApi();
useEffect(() => {
setType(env.type);
}, [env.type]);
const history = useHistory();
const { name, type, setName, setType, errors, clearErrors } =
useEnvironmentForm(environment.name, environment.type);
const { refetch } = useProjectRolePermissions();
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
setType(event.currentTarget.value);
};
const isDisabled = () => {
return type === env.type;
};
const handleCancel = () => {
setEditEnvironment(false);
resetFields();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const updatedEnv = {
sortOrder: env.sortOrder,
const editPayload = () => {
return {
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 {
await updateEnvironment(env.name, updatedEnv);
await updateEnvironment(id, editPayload());
refetch();
history.push('/environments');
setToastData({
type: 'success',
show: true,
text: 'Successfully updated environment.',
title: 'Successfully updated environment.',
});
resetFields();
refetch();
} catch (e) {
setToastData({
show: true,
type: 'error',
text: e.toString(),
});
} finally {
setEditEnvironment(false);
} catch (e: any) {
setToastApiError(e.toString());
}
};
const resetFields = () => {
setType(env.type);
const handleCancel = () => {
history.goBack();
};
const formId = 'edit-environment-form';
return (
<Dialogue
open={editEnvironment}
<FormTemplate
title="Edit environment"
onClose={handleCancel}
onClick={handleSubmit}
primaryButtonText="Save"
secondaryButtonText="Cancel"
disabledPrimaryButton={isDisabled()}
formId={formId}
description="Environments allow you to manage your
product lifecycle from local development
through production. Your projects and
feature toggles are accessible in all your
environments, but they can take different
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}>
<h3 className={styles.formHeader} data-loading>
Environment Id
</h3>
<h3 className={styles.subheader} data-loading>
<CloudCircle className={styles.icon} /> {env.name}
</h3>
<form id={formId}>
<EnvironmentTypeSelector
onChange={handleTypeChange}
value={type}
/>
</form>
</div>
</Dialogue>
<EnvironmentForm
handleSubmit={handleSubmit}
handleCancel={handleCancel}
name={name}
type={type}
setName={setName}
setType={setType}
mode="Edit"
errors={errors}
clearErrors={clearErrors}
>
<PermissionButton
onClick={handleSubmit}
permission={ADMIN}
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: {
fontWeight: 'bold',
//@ts-ignore
fontSize: theme.fontSizes.bodySize,
marginTop: '1.5rem',
marginBottom: '0.5rem',

View File

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

View File

@ -1,12 +1,13 @@
import { CloudCircle } from '@material-ui/icons';
import StringTruncator from '../../../../common/StringTruncator/StringTruncator';
import { ICreateEnvironmentSuccessProps } from '../CreateEnvironmentSuccess';
import { useStyles } from './CreateEnvironmentSuccessCard.styles';
import StringTruncator from '../../../common/StringTruncator/StringTruncator';
import { useStyles } from './EnvironmentCard.styles';
const CreateEnvironmentSuccessCard = ({
name,
type,
}: ICreateEnvironmentSuccessProps) => {
interface IEnvironmentProps {
name: string;
type: string;
}
const EnvironmentCard = ({ name, type }: IEnvironmentProps) => {
const styles = useStyles();
return (
<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 Dialogue from '../../../common/Dialogue';
import Input from '../../../common/Input/Input';
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
import { useStyles } from './EnvironmentDeleteConfirm.styles';
interface IEnviromentDeleteConfirmProps {
@ -52,7 +52,7 @@ const EnvironmentDeleteConfirm = ({
strategies that are active in this environment across all
feature toggles.
</Alert>
<CreateEnvironmentSuccessCard name={env?.name} type={env?.type} />
<EnvironmentCard name={env?.name} type={env?.type} />
<p className={styles.deleteParagraph}>
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 EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
import { mutate } from 'swr';
import EditEnvironment from '../EditEnvironment/EditEnvironment';
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
@ -31,7 +30,6 @@ const EnvironmentList = () => {
protected: false,
};
const { environments, refetch } = useEnvironments();
const [editEnvironment, setEditEnvironment] = useState(false);
const { refetch: refetchProjectRolePermissions } =
useProjectRolePermissions();
@ -151,7 +149,6 @@ const EnvironmentList = () => {
<EnvironmentListItem
key={env.name}
env={env}
setEditEnvironment={setEditEnvironment}
setDeldialogue={setDeldialogue}
setSelectedEnv={setSelectedEnv}
setToggleDialog={setToggleDialog}
@ -195,13 +192,6 @@ const EnvironmentList = () => {
confirmName={confirmName}
setConfirmName={setConfirmName}
/>
<EditEnvironment
env={selectedEnv}
setEditEnvironment={setEditEnvironment}
editEnvironment={editEnvironment}
setToastData={setToastData}
/>
<EnvironmentToggleConfirm
env={selectedEnv}
open={toggleDialog}

View File

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

View File

@ -4,7 +4,7 @@ import React from 'react';
import { IEnvironment } from '../../../../interfaces/environments';
import ConditionallyRender from '../../../common/ConditionallyRender';
import Dialogue from '../../../common/Dialogue';
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
interface IEnvironmentToggleConfirmProps {
env: IEnvironment;
@ -52,10 +52,7 @@ const EnvironmentToggleConfirm = ({
}
/>
<CreateEnvironmentSuccessCard
name={env?.name}
type={env?.type}
/>
<EnvironmentCard name={env?.name} type={env?.type} />
</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",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"path": "/environments/:id",
"title": "Edit",
"type": "protected",
},
Object {
"component": [Function],
"flag": "EEA",
@ -380,6 +388,15 @@ Array [
"title": "Users",
"type": "protected",
},
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"parent": "/admin",
"path": "/admin/create-user",
"title": "Users",
"type": "protected",
},
Object {
"component": Object {
"$$typeof": Symbol(react.memo),

View File

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