1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-18 01:18:23 +02:00

feat: add create and edit environment screen (NEW) (#605)

* feat: add create and edit environment screen

* fix: remove environment success screen

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2022-01-18 11:23:24 +01:00 committed by GitHub
parent 114542803d
commit 1b097f85d6
21 changed files with 476 additions and 501 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -214,6 +214,14 @@ Array [
"title": "Environments", "title": "Environments",
"type": "protected", "type": "protected",
}, },
Object {
"component": [Function],
"layout": "main",
"menu": Object {},
"path": "/environments/:id",
"title": "Edit",
"type": "protected",
},
Object { Object {
"component": [Function], "component": [Function],
"flag": "EEA", "flag": "EEA",

View File

@ -37,13 +37,15 @@ 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 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 +257,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',

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;