From 1b097f85d63c6b8c8bebaa57ac9fed50240e65e3 Mon Sep 17 00:00:00 2001 From: Youssef Khedher Date: Tue, 18 Jan 2022 11:23:24 +0100 Subject: [PATCH] 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 --- .../CreateEnvironment.styles.ts | 31 -- .../CreateEnvironment/CreateEnvironment.tsx | 282 +++++++----------- .../CreateEnvironmentSuccess.styles.ts | 41 --- .../CreateEnvironmentSuccess.tsx | 89 ------ .../EditEnvironment/EditEnvironment.styles.ts | 54 ---- .../EditEnvironment/EditEnvironment.tsx | 161 +++++----- .../EnvironmentForm/EnvironmentForm.styles.ts | 47 +++ .../EnvironmentForm/EnvironmentForm.tsx | 74 +++++ .../EnvironmentTypeSelector.styles.ts | 1 + .../EnvironmentTypeSelector.tsx | 4 - .../EnvironmentCard.styles.ts} | 0 .../EnvironmentCard/EnvironmentCard.tsx} | 17 +- .../EnvironmentDeleteConfirm.tsx | 4 +- .../EnvironmentList/EnvironmentList.tsx | 10 - .../EnvironmentListItem.tsx | 7 +- .../EnvironmentToggleConfirm.tsx | 7 +- .../environments/hooks/useEnvironmentForm.ts | 69 +++++ .../__snapshots__/routes-test.jsx.snap | 8 + frontend/src/component/menu/routes.js | 12 +- .../useEnvironment/defaultEnvironment.ts | 10 + .../getters/useEnvironment/useEnvironment.ts | 49 +++ 21 files changed, 476 insertions(+), 501 deletions(-) delete mode 100644 frontend/src/component/environments/CreateEnvironment/CreateEnvironment.styles.ts delete mode 100644 frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.styles.ts delete mode 100644 frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.tsx delete mode 100644 frontend/src/component/environments/EditEnvironment/EditEnvironment.styles.ts create mode 100644 frontend/src/component/environments/EnvironmentForm/EnvironmentForm.styles.ts create mode 100644 frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx rename frontend/src/component/environments/{form => EnvironmentForm}/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts (94%) rename frontend/src/component/environments/{form => EnvironmentForm}/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx (93%) rename frontend/src/component/environments/{CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard.styles.ts => EnvironmentList/EnvironmentCard/EnvironmentCard.styles.ts} (100%) rename frontend/src/component/environments/{CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccessCard/CreateEnvironmentSuccessCard.tsx => EnvironmentList/EnvironmentCard/EnvironmentCard.tsx} (68%) create mode 100644 frontend/src/component/environments/hooks/useEnvironmentForm.ts create mode 100644 frontend/src/hooks/api/getters/useEnvironment/defaultEnvironment.ts create mode 100644 frontend/src/hooks/api/getters/useEnvironment/useEnvironment.ts diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.styles.ts b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.styles.ts deleted file mode 100644 index 76b1198f49..0000000000 --- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.styles.ts +++ /dev/null @@ -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', - }, -})); diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx index 22706fbb3f..4e9992b041 100644 --- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx +++ b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx @@ -1,193 +1,141 @@ -import React, { useState } from 'react'; -import { FormControl, Button } from '@material-ui/core'; -import HeaderTitle from '../../common/HeaderTitle'; -import PageContent from '../../common/PageContent'; - -import { useStyles } from './CreateEnvironment.styles'; import { useHistory } from 'react-router-dom'; -import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; -import ConditionallyRender from '../../common/ConditionallyRender'; -import CreateEnvironmentSuccess from './CreateEnvironmentSuccess/CreateEnvironmentSuccess'; -import useLoading from '../../../hooks/useLoading'; +import useEnvironmentForm from '../hooks/useEnvironmentForm'; +import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; import useToast from '../../../hooks/useToast'; -import EnvironmentTypeSelector from '../form/EnvironmentTypeSelector/EnvironmentTypeSelector'; -import Input from '../../common/Input/Input'; +import useEnvironmentApi from '../../../hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; +import EnvironmentForm from '../EnvironmentForm/EnvironmentForm'; +import FormTemplate from '../../common/FormTemplate/FormTemplate'; import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments'; import { Alert } from '@material-ui/lab'; +import { Button } from '@material-ui/core'; +import ConditionallyRender from '../../common/ConditionallyRender'; +import PageContent from '../../common/PageContent'; +import HeaderTitle from '../../common/HeaderTitle'; +import PermissionButton from '../../common/PermissionButton/PermissionButton'; +import { ADMIN } from '../../providers/AccessProvider/permissions'; import useProjectRolePermissions from '../../../hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; -const NAME_EXISTS_ERROR = 'Error: Environment'; - const CreateEnvironment = () => { - const [type, setType] = useState('development'); - const [envName, setEnvName] = useState(''); - const [nameError, setNameError] = useState(''); - const [createSuccess, setCreateSucceess] = useState(false); + /* @ts-ignore */ + const { setToastApiError, setToastData } = useToast(); + const { uiConfig } = useUiConfig(); const history = useHistory(); - const styles = useStyles(); - const { validateEnvName, createEnvironment, loading } = useEnvironmentApi(); const { environments } = useEnvironments(); - const ref = useLoading(loading); - const { setToastApiError } = useToast(); - const { refetch } = useProjectRolePermissions(); - - const handleTypeChange = (event: React.FormEvent) => { - setType(event.currentTarget.value); - }; - - const handleEnvNameChange = (e: React.FormEvent) => { - setEnvName(e.currentTarget.value); - }; - - const goBack = () => history.goBack(); - const canCreateMoreEnvs = environments.length < 7; + const { createEnvironment, loading } = useEnvironmentApi(); + const { refetch } = useProjectRolePermissions(); + const { + name, + setName, + type, + setType, + getEnvPayload, + validateEnvironmentName, + clearErrors, + errors, + } = useEnvironmentForm(); - const validateEnvironmentName = async () => { - if (envName.length === 0) { - setNameError('Environment Id can not be empty.'); - return false; - } - - try { - await validateEnvName(envName); - } catch (e) { - if (e.toString().includes(NAME_EXISTS_ERROR)) { - setNameError('Name already exists'); - } - return false; - } - return true; - }; - - const clearNameError = () => setNameError(''); - - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: Event) => { e.preventDefault(); + clearErrors(); const validName = await validateEnvironmentName(); - if (validName) { - const environment = { - name: envName, - type, - }; - + const payload = getEnvPayload(); try { - await createEnvironment(environment); + await createEnvironment(payload); refetch(); - setCreateSucceess(true); - } catch (e) { + setToastData({ + title: 'Environment created', + type: 'success', + confetti: true, + }); + history.push('/environments'); + } catch (e: any) { setToastApiError(e.toString()); } } }; + const formatApiCode = () => { + return `curl --location --request POST '${ + uiConfig.unleashUrl + }/api/admin/environments' \\ +--header 'Authorization: INSERT_API_KEY' \\ +--header 'Content-Type: application/json' \\ +--data-raw '${JSON.stringify(getEnvPayload(), undefined, 2)}'`; + }; + + const handleCancel = () => { + history.goBack(); + }; + return ( - }> - } - elseShow={ - -

- 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. -

- -
- -

- Environment Id and name -

- -
-

- Unique env name for SDK - configurations. -

- -
- - -
-
- {' '} - -
-
- + + + + Create environment + + + + } + elseShow={ + <> + } - elseShow={ - <> - -

- 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. +

+
+
+ +
+ + } + /> ); }; diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.styles.ts b/frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.styles.ts deleted file mode 100644 index f4a82266f7..0000000000 --- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.styles.ts +++ /dev/null @@ -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, - }, -})); diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.tsx b/frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.tsx deleted file mode 100644 index 9ebadcfc86..0000000000 --- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironmentSuccess/CreateEnvironmentSuccess.tsx +++ /dev/null @@ -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 ( -
- -

Environment created

- -

Next steps

-
-
-
-
1
-

- Update SDK version and provide the environment id to - the SDK -

-

- By providing the environment id in the SDK the SDK - will only retrieve activation strategies for - specified environment -

- - Learn more - -
-
-
-
-
2
-

- Add environment specific activation strategies -

- -

- You can now select this environment when you are - adding new activation strategies on feature toggles. -

- - Learn more - -
-
-
- - -
- ); -}; - -export default CreateEnvironmentSuccess; diff --git a/frontend/src/component/environments/EditEnvironment/EditEnvironment.styles.ts b/frontend/src/component/environments/EditEnvironment/EditEnvironment.styles.ts deleted file mode 100644 index c1e73b0656..0000000000 --- a/frontend/src/component/environments/EditEnvironment/EditEnvironment.styles.ts +++ /dev/null @@ -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', - }, -})); diff --git a/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx b/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx index 4940910c1f..e443922515 100644 --- a/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx +++ b/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx @@ -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>; - editEnvironment: boolean; - setToastData: React.Dispatch>; -} +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) => { - 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 ( - -
-

- Environment Id -

-

- {env.name} -

-
- - -
-
+ + + Edit environment + + + ); }; diff --git a/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.styles.ts b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.styles.ts new file mode 100644 index 0000000000..1345735a57 --- /dev/null +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.styles.ts @@ -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', + }, +})); diff --git a/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx new file mode 100644 index 0000000000..acbec5c027 --- /dev/null +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx @@ -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>; + setType: React.Dispatch>; + validateEnvironmentName?: (e: any) => void; + handleSubmit: (e: any) => void; + handleCancel: () => void; + errors: { [key: string]: string }; + mode: string; + clearErrors: () => void; +} + +const EnvironmentForm: React.FC = ({ + children, + handleSubmit, + handleCancel, + name, + type, + setName, + setType, + validateEnvironmentName, + errors, + mode, + clearErrors, +}) => { + const styles = useStyles(); + + return ( +
+

Environment information

+ +
+

+ What is your environment name? (Can't be changed later) +

+ setName(trim(e.target.value))} + error={Boolean(errors.name)} + errorText={errors.name} + onFocus={() => clearErrors()} + onBlur={validateEnvironmentName} + disabled={mode === 'Edit'} + /> + +

+ What type of environment do you want to create? +

+ setType(e.currentTarget.value)} + value={type} + /> +
+
+ + {children} +
+
+ ); +}; + +export default EnvironmentForm; diff --git a/frontend/src/component/environments/form/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts similarity index 94% rename from frontend/src/component/environments/form/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts rename to frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts index 11aabd3422..3e19ac92b1 100644 --- a/frontend/src/component/environments/form/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.styles.ts @@ -6,6 +6,7 @@ export const useStyles = makeStyles(theme => ({ }, formHeader: { fontWeight: 'bold', + //@ts-ignore fontSize: theme.fontSizes.bodySize, marginTop: '1.5rem', marginBottom: '0.5rem', diff --git a/frontend/src/component/environments/form/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx similarity index 93% rename from frontend/src/component/environments/form/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx rename to frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx index bfa0154990..07353874f2 100644 --- a/frontend/src/component/environments/form/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx @@ -18,10 +18,6 @@ const EnvironmentTypeSelector = ({ const styles = useStyles(); return ( -

- Environment Type -

- { +interface IEnvironmentProps { + name: string; + type: string; +} + +const EnvironmentCard = ({ name, type }: IEnvironmentProps) => { const styles = useStyles(); return (
@@ -30,4 +31,4 @@ const CreateEnvironmentSuccessCard = ({ ); }; -export default CreateEnvironmentSuccessCard; +export default EnvironmentCard; diff --git a/frontend/src/component/environments/EnvironmentList/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.tsx b/frontend/src/component/environments/EnvironmentList/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.tsx index 147e582643..61196f50c4 100644 --- a/frontend/src/component/environments/EnvironmentList/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.tsx +++ b/frontend/src/component/environments/EnvironmentList/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.tsx @@ -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. - +

In order to delete this environment, please enter the id of the diff --git a/frontend/src/component/environments/EnvironmentList/EnvironmentList.tsx b/frontend/src/component/environments/EnvironmentList/EnvironmentList.tsx index f951fec3e3..691946e0de 100644 --- a/frontend/src/component/environments/EnvironmentList/EnvironmentList.tsx +++ b/frontend/src/component/environments/EnvironmentList/EnvironmentList.tsx @@ -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 = () => { { confirmName={confirmName} setConfirmName={setConfirmName} /> - - >; setDeldialogue: React.Dispatch>; - setEditEnvironment: React.Dispatch>; setToggleDialog: React.Dispatch>; 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(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}`); }} > diff --git a/frontend/src/component/environments/EnvironmentList/EnvironmentToggleConfirm/EnvironmentToggleConfirm.tsx b/frontend/src/component/environments/EnvironmentList/EnvironmentToggleConfirm/EnvironmentToggleConfirm.tsx index ed7c77ef7c..0414700f05 100644 --- a/frontend/src/component/environments/EnvironmentList/EnvironmentToggleConfirm/EnvironmentToggleConfirm.tsx +++ b/frontend/src/component/environments/EnvironmentList/EnvironmentToggleConfirm/EnvironmentToggleConfirm.tsx @@ -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 = ({ } /> - + ); }; diff --git a/frontend/src/component/environments/hooks/useEnvironmentForm.ts b/frontend/src/component/environments/hooks/useEnvironmentForm.ts new file mode 100644 index 0000000000..71c3b51800 --- /dev/null +++ b/frontend/src/component/environments/hooks/useEnvironmentForm.ts @@ -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; diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap index 21d6a88a07..02bc53fc68 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap @@ -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", diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 3898f3eff8..63fa2fc10d 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -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', diff --git a/frontend/src/hooks/api/getters/useEnvironment/defaultEnvironment.ts b/frontend/src/hooks/api/getters/useEnvironment/defaultEnvironment.ts new file mode 100644 index 0000000000..defeee0c8c --- /dev/null +++ b/frontend/src/hooks/api/getters/useEnvironment/defaultEnvironment.ts @@ -0,0 +1,10 @@ +import { IEnvironment } from '../../../../interfaces/environments'; + +export const defaultEnvironment: IEnvironment = { + name: '', + type: '', + createdAt: '', + sortOrder: 0, + enabled: false, + protected: false +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/getters/useEnvironment/useEnvironment.ts b/frontend/src/hooks/api/getters/useEnvironment/useEnvironment.ts new file mode 100644 index 0000000000..19b2f1e3a4 --- /dev/null +++ b/frontend/src/hooks/api/getters/useEnvironment/useEnvironment.ts @@ -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(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;