mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01: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:
parent
114542803d
commit
1b097f85d6
@ -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,99 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormControl, Button } from '@material-ui/core';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import PageContent from '../../common/PageContent';
|
||||
|
||||
import { useStyles } from './CreateEnvironment.styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import CreateEnvironmentSuccess from './CreateEnvironmentSuccess/CreateEnvironmentSuccess';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import useEnvironmentForm from '../hooks/useEnvironmentForm';
|
||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
|
||||
import Input from '../../common/Input/Input';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
|
||||
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { Button } from '@material-ui/core';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import PageContent from '../../common/PageContent';
|
||||
import HeaderTitle from '../../common/HeaderTitle';
|
||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
|
||||
const NAME_EXISTS_ERROR = 'Error: Environment';
|
||||
|
||||
const CreateEnvironment = () => {
|
||||
const [type, setType] = useState('development');
|
||||
const [envName, setEnvName] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [createSuccess, setCreateSucceess] = useState(false);
|
||||
/* @ts-ignore */
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const history = useHistory();
|
||||
const styles = useStyles();
|
||||
const { validateEnvName, createEnvironment, loading } = useEnvironmentApi();
|
||||
const { environments } = useEnvironments();
|
||||
const ref = useLoading(loading);
|
||||
const { setToastApiError } = useToast();
|
||||
const { refetch } = useProjectRolePermissions();
|
||||
|
||||
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
setType(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleEnvNameChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
setEnvName(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const goBack = () => history.goBack();
|
||||
|
||||
const canCreateMoreEnvs = environments.length < 7;
|
||||
|
||||
const validateEnvironmentName = async () => {
|
||||
if (envName.length === 0) {
|
||||
setNameError('Environment Id can not be empty.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await validateEnvName(envName);
|
||||
} catch (e) {
|
||||
if (e.toString().includes(NAME_EXISTS_ERROR)) {
|
||||
setNameError('Name already exists');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const clearNameError = () => setNameError('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const validName = await validateEnvironmentName();
|
||||
|
||||
if (validName) {
|
||||
const environment = {
|
||||
name: envName,
|
||||
const { createEnvironment, loading } = useEnvironmentApi();
|
||||
const { refetch } = useProjectRolePermissions();
|
||||
const {
|
||||
name,
|
||||
setName,
|
||||
type,
|
||||
};
|
||||
setType,
|
||||
getEnvPayload,
|
||||
validateEnvironmentName,
|
||||
clearErrors,
|
||||
errors,
|
||||
} = useEnvironmentForm();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
const validName = await validateEnvironmentName();
|
||||
if (validName) {
|
||||
const payload = getEnvPayload();
|
||||
try {
|
||||
await createEnvironment(environment);
|
||||
await createEnvironment(payload);
|
||||
refetch();
|
||||
setCreateSucceess(true);
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
title: 'Environment created',
|
||||
type: 'success',
|
||||
confetti: true,
|
||||
});
|
||||
history.push('/environments');
|
||||
} catch (e: any) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request POST '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/environments' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getEnvPayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent headerContent={<HeaderTitle title="Create environment" />}>
|
||||
<ConditionallyRender
|
||||
condition={createSuccess}
|
||||
show={<CreateEnvironmentSuccess name={envName} type={type} />}
|
||||
elseShow={
|
||||
<ConditionallyRender
|
||||
condition={canCreateMoreEnvs}
|
||||
show={
|
||||
<div ref={ref}>
|
||||
<p className={styles.helperText} data-loading>
|
||||
Environments allow you to manage your
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Create Environment"
|
||||
description="Environments allow you to manage your
|
||||
product lifecycle from local development
|
||||
through production. Your projects and
|
||||
feature toggles are accessible in all your
|
||||
@ -102,92 +85,57 @@ const CreateEnvironment = () => {
|
||||
that you can enable a feature toggle in a
|
||||
development or test environment without
|
||||
enabling the feature toggle in the
|
||||
production environment.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormControl component="fieldset">
|
||||
<h3
|
||||
className={styles.formHeader}
|
||||
data-loading
|
||||
production environment."
|
||||
documentationLink="https://docs.getunleash.io/user_guide/environments"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
Environment Id and name
|
||||
</h3>
|
||||
|
||||
<div
|
||||
data-loading
|
||||
className={
|
||||
styles.environmentDetailsContainer
|
||||
}
|
||||
<EnvironmentForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
validateEnvironmentName={validateEnvironmentName}
|
||||
name={name}
|
||||
type={type}
|
||||
setName={setName}
|
||||
setType={setType}
|
||||
mode="Create"
|
||||
clearErrors={clearErrors}
|
||||
>
|
||||
<p>
|
||||
Unique env name for SDK
|
||||
configurations.
|
||||
</p>
|
||||
<Input
|
||||
label="Environment Id"
|
||||
onFocus={clearNameError}
|
||||
placeholder="A unique name for your environment"
|
||||
onBlur={validateEnvironmentName}
|
||||
error={Boolean(nameError)}
|
||||
errorText={nameError}
|
||||
value={envName}
|
||||
onChange={handleEnvNameChange}
|
||||
className={styles.inputField}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EnvironmentTypeSelector
|
||||
onChange={handleTypeChange}
|
||||
value={type}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className={styles.btnContainer}>
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
<PermissionButton
|
||||
onClick={handleSubmit}
|
||||
permission={ADMIN}
|
||||
type="submit"
|
||||
data-loading
|
||||
>
|
||||
Submit
|
||||
</Button>{' '}
|
||||
<Button
|
||||
className={styles.submitButton}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={goBack}
|
||||
data-loading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Create environment
|
||||
</PermissionButton>
|
||||
</EnvironmentForm>
|
||||
</FormTemplate>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<PageContent
|
||||
headerContent={
|
||||
<HeaderTitle title="Create environment" />
|
||||
}
|
||||
>
|
||||
<Alert severity="error">
|
||||
<p>
|
||||
Currently Unleash does not support more
|
||||
than 5 environments. If you need more
|
||||
please reach out.
|
||||
Currently Unleash does not support more than 7
|
||||
environments. If you need more please reach out.
|
||||
</p>
|
||||
</Alert>
|
||||
<br />
|
||||
<Button
|
||||
onClick={goBack}
|
||||
onClick={handleCancel}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</PageContent>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</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 { useEffect, useState } from 'react';
|
||||
import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector';
|
||||
import { useStyles } from './EditEnvironment.styles';
|
||||
import { IEnvironment } from '../../../interfaces/environments';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import Dialogue from '../../common/Dialogue';
|
||||
import useEnvironment from '../../../hooks/api/getters/useEnvironment/useEnvironment';
|
||||
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import EnvironmentForm from '../EnvironmentForm/EnvironmentForm';
|
||||
import useEnvironmentForm from '../hooks/useEnvironmentForm';
|
||||
|
||||
interface IEditEnvironmentProps {
|
||||
env: IEnvironment;
|
||||
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
editEnvironment: boolean;
|
||||
setToastData: React.Dispatch<React.SetStateAction<any>>;
|
||||
}
|
||||
const EditEnvironment = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
const EditEnvironment = ({
|
||||
env,
|
||||
setEditEnvironment,
|
||||
editEnvironment,
|
||||
setToastData,
|
||||
}: IEditEnvironmentProps) => {
|
||||
const styles = useStyles();
|
||||
const [type, setType] = useState(env.type);
|
||||
const { updateEnvironment, loading } = useEnvironmentApi();
|
||||
const { refetch } = useEnvironments();
|
||||
const ref = useLoading(loading);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { environment } = useEnvironment(id);
|
||||
const { updateEnvironment } = useEnvironmentApi();
|
||||
|
||||
useEffect(() => {
|
||||
setType(env.type);
|
||||
}, [env.type]);
|
||||
const history = useHistory();
|
||||
const { name, type, setName, setType, errors, clearErrors } =
|
||||
useEnvironmentForm(environment.name, environment.type);
|
||||
const { refetch } = useProjectRolePermissions();
|
||||
|
||||
const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
setType(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const isDisabled = () => {
|
||||
return type === env.type;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditEnvironment(false);
|
||||
resetFields();
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const updatedEnv = {
|
||||
sortOrder: env.sortOrder,
|
||||
const editPayload = () => {
|
||||
return {
|
||||
type,
|
||||
sortOrder: environment.sortOrder,
|
||||
};
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/environments/update/${id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(editPayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await updateEnvironment(env.name, updatedEnv);
|
||||
await updateEnvironment(id, editPayload());
|
||||
refetch();
|
||||
history.push('/environments');
|
||||
setToastData({
|
||||
type: 'success',
|
||||
show: true,
|
||||
text: 'Successfully updated environment.',
|
||||
title: 'Successfully updated environment.',
|
||||
});
|
||||
resetFields();
|
||||
refetch();
|
||||
} catch (e) {
|
||||
setToastData({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: e.toString(),
|
||||
});
|
||||
} finally {
|
||||
setEditEnvironment(false);
|
||||
} catch (e: any) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const resetFields = () => {
|
||||
setType(env.type);
|
||||
const handleCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
const formId = 'edit-environment-form';
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={editEnvironment}
|
||||
<FormTemplate
|
||||
title="Edit environment"
|
||||
onClose={handleCancel}
|
||||
onClick={handleSubmit}
|
||||
primaryButtonText="Save"
|
||||
secondaryButtonText="Cancel"
|
||||
disabledPrimaryButton={isDisabled()}
|
||||
formId={formId}
|
||||
description="Environments allow you to manage your
|
||||
product lifecycle from local development
|
||||
through production. Your projects and
|
||||
feature toggles are accessible in all your
|
||||
environments, but they can take different
|
||||
configurations per environment. This means
|
||||
that you can enable a feature toggle in a
|
||||
development or test environment without
|
||||
enabling the feature toggle in the
|
||||
production environment."
|
||||
documentationLink="https://docs.getunleash.io/user_guide/environments"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<div className={styles.body} ref={ref}>
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment Id
|
||||
</h3>
|
||||
<h3 className={styles.subheader} data-loading>
|
||||
<CloudCircle className={styles.icon} /> {env.name}
|
||||
</h3>
|
||||
<form id={formId}>
|
||||
<EnvironmentTypeSelector
|
||||
onChange={handleTypeChange}
|
||||
value={type}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</Dialogue>
|
||||
<EnvironmentForm
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
name={name}
|
||||
type={type}
|
||||
setName={setName}
|
||||
setType={setType}
|
||||
mode="Edit"
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
>
|
||||
<PermissionButton
|
||||
onClick={handleSubmit}
|
||||
permission={ADMIN}
|
||||
type="submit"
|
||||
>
|
||||
Edit environment
|
||||
</PermissionButton>
|
||||
</EnvironmentForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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: {
|
||||
fontWeight: 'bold',
|
||||
//@ts-ignore
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
marginTop: '1.5rem',
|
||||
marginBottom: '0.5rem',
|
@ -18,10 +18,6 @@ const EnvironmentTypeSelector = ({
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<FormControl component="fieldset">
|
||||
<h3 className={styles.formHeader} data-loading>
|
||||
Environment Type
|
||||
</h3>
|
||||
|
||||
<RadioGroup
|
||||
data-loading
|
||||
value={value}
|
@ -1,12 +1,13 @@
|
||||
import { CloudCircle } from '@material-ui/icons';
|
||||
import StringTruncator from '../../../../common/StringTruncator/StringTruncator';
|
||||
import { ICreateEnvironmentSuccessProps } from '../CreateEnvironmentSuccess';
|
||||
import { useStyles } from './CreateEnvironmentSuccessCard.styles';
|
||||
import StringTruncator from '../../../common/StringTruncator/StringTruncator';
|
||||
import { useStyles } from './EnvironmentCard.styles';
|
||||
|
||||
const CreateEnvironmentSuccessCard = ({
|
||||
name,
|
||||
type,
|
||||
}: ICreateEnvironmentSuccessProps) => {
|
||||
interface IEnvironmentProps {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const EnvironmentCard = ({ name, type }: IEnvironmentProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -30,4 +31,4 @@ const CreateEnvironmentSuccessCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEnvironmentSuccessCard;
|
||||
export default EnvironmentCard;
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { IEnvironment } from '../../../../interfaces/environments';
|
||||
import Dialogue from '../../../common/Dialogue';
|
||||
import Input from '../../../common/Input/Input';
|
||||
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
|
||||
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
|
||||
import { useStyles } from './EnvironmentDeleteConfirm.styles';
|
||||
|
||||
interface IEnviromentDeleteConfirmProps {
|
||||
@ -52,7 +52,7 @@ const EnvironmentDeleteConfirm = ({
|
||||
strategies that are active in this environment across all
|
||||
feature toggles.
|
||||
</Alert>
|
||||
<CreateEnvironmentSuccessCard name={env?.name} type={env?.type} />
|
||||
<EnvironmentCard name={env?.name} type={env?.type} />
|
||||
|
||||
<p className={styles.deleteParagraph}>
|
||||
In order to delete this environment, please enter the id of the
|
||||
|
@ -17,7 +17,6 @@ import useToast from '../../../hooks/useToast';
|
||||
import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
|
||||
import { mutate } from 'swr';
|
||||
import EditEnvironment from '../EditEnvironment/EditEnvironment';
|
||||
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
||||
import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
|
||||
@ -31,7 +30,6 @@ const EnvironmentList = () => {
|
||||
protected: false,
|
||||
};
|
||||
const { environments, refetch } = useEnvironments();
|
||||
const [editEnvironment, setEditEnvironment] = useState(false);
|
||||
const { refetch: refetchProjectRolePermissions } =
|
||||
useProjectRolePermissions();
|
||||
|
||||
@ -151,7 +149,6 @@ const EnvironmentList = () => {
|
||||
<EnvironmentListItem
|
||||
key={env.name}
|
||||
env={env}
|
||||
setEditEnvironment={setEditEnvironment}
|
||||
setDeldialogue={setDeldialogue}
|
||||
setSelectedEnv={setSelectedEnv}
|
||||
setToggleDialog={setToggleDialog}
|
||||
@ -195,13 +192,6 @@ const EnvironmentList = () => {
|
||||
confirmName={confirmName}
|
||||
setConfirmName={setConfirmName}
|
||||
/>
|
||||
|
||||
<EditEnvironment
|
||||
env={selectedEnv}
|
||||
setEditEnvironment={setEditEnvironment}
|
||||
editEnvironment={editEnvironment}
|
||||
setToastData={setToastData}
|
||||
/>
|
||||
<EnvironmentToggleConfirm
|
||||
env={selectedEnv}
|
||||
open={toggleDialog}
|
||||
|
@ -25,12 +25,12 @@ import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
|
||||
import { XYCoord } from 'dnd-core';
|
||||
import DisabledIndicator from '../../../common/DisabledIndicator/DisabledIndicator';
|
||||
import StringTruncator from '../../../common/StringTruncator/StringTruncator';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
interface IEnvironmentListItemProps {
|
||||
env: IEnvironment;
|
||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setEditEnvironment: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
index: number;
|
||||
moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[];
|
||||
@ -51,8 +51,8 @@ const EnvironmentListItem = ({
|
||||
moveListItem,
|
||||
moveListItemApi,
|
||||
setToggleDialog,
|
||||
setEditEnvironment,
|
||||
}: IEnvironmentListItemProps) => {
|
||||
const history = useHistory();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ACCEPT_TYPE = 'LIST_ITEM';
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
@ -178,8 +178,7 @@ const EnvironmentListItem = ({
|
||||
aria-label="update"
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
setSelectedEnv(env);
|
||||
setEditEnvironment(prev => !prev);
|
||||
history.push(`/environments/${env.name}`);
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { IEnvironment } from '../../../../interfaces/environments';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import Dialogue from '../../../common/Dialogue';
|
||||
import CreateEnvironmentSuccessCard from '../../CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard';
|
||||
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
|
||||
|
||||
interface IEnvironmentToggleConfirmProps {
|
||||
env: IEnvironment;
|
||||
@ -52,10 +52,7 @@ const EnvironmentToggleConfirm = ({
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateEnvironmentSuccessCard
|
||||
name={env?.name}
|
||||
type={env?.type}
|
||||
/>
|
||||
<EnvironmentCard name={env?.name} type={env?.type} />
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
@ -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",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"path": "/environments/:id",
|
||||
"title": "Edit",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"flag": "EEA",
|
||||
|
@ -37,13 +37,15 @@ import Project from '../project/Project/Project';
|
||||
import RedirectFeatureViewPage from '../../page/features/redirect';
|
||||
import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
|
||||
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
|
||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
|
||||
import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
|
||||
import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles';
|
||||
import CreateProjectRole from '../admin/project-roles/CreateProjectRole/CreateProjectRole';
|
||||
import EditProjectRole from '../admin/project-roles/EditProjectRole/EditProjectRole';
|
||||
import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken';
|
||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
||||
|
||||
|
||||
export const routes = [
|
||||
// Project
|
||||
@ -255,6 +257,14 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/environments/:id',
|
||||
title: 'Edit',
|
||||
component: EditEnvironment,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/environments',
|
||||
title: 'Environments',
|
||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user