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:
commit
dd64f7110f
@ -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;
|
|
@ -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;
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}));
|
|
119
frontend/src/component/admin/users/CreateUser/CreateUser.tsx
Normal file
119
frontend/src/component/admin/users/CreateUser/CreateUser.tsx
Normal 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;
|
114
frontend/src/component/admin/users/EditUser/EditUser.tsx
Normal file
114
frontend/src/component/admin/users/EditUser/EditUser.tsx
Normal 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;
|
@ -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',
|
||||||
|
},
|
||||||
|
}));
|
161
frontend/src/component/admin/users/UserForm/UserForm.tsx
Normal file
161
frontend/src/component/admin/users/UserForm/UserForm.tsx
Normal 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;
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
84
frontend/src/component/admin/users/hooks/useAddUserForm.ts
Normal file
84
frontend/src/component/admin/users/hooks/useAddUserForm.ts
Normal 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;
|
@ -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>
|
||||||
|
@ -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;
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
}));
|
|
@ -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;
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
|
}));
|
@ -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;
|
@ -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',
|
@ -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}
|
@ -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;
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
@ -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),
|
||||||
|
@ -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',
|
||||||
|
2
frontend/src/constants/misc.ts
Normal file
2
frontend/src/constants/misc.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const EDIT = 'Edit';
|
||||||
|
export const CREATE = 'Create';
|
@ -0,0 +1,10 @@
|
|||||||
|
import { IEnvironment } from '../../../../interfaces/environments';
|
||||||
|
|
||||||
|
export const defaultEnvironment: IEnvironment = {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
createdAt: '',
|
||||||
|
sortOrder: 0,
|
||||||
|
enabled: false,
|
||||||
|
protected: false
|
||||||
|
};
|
@ -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;
|
35
frontend/src/hooks/api/getters/useUserInfo/useUserInfo.ts
Normal file
35
frontend/src/hooks/api/getters/useUserInfo/useUserInfo.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user