mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat clone environment modal (#2245)
* add clone environment modal base skeleton (WIP) * refactor HelpIcon common component, fix group form * add more fields to clone env modal, multi project selector * implement initial payload signature * reflect latest design decisions * misc ui fixes * update UI to the new designs, change back clone option to use flag * set env limit to 15 * Update frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx Co-authored-by: Simon Hornby <liquidwicked64@gmail.com> * Update frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx Co-authored-by: Simon Hornby <liquidwicked64@gmail.com> * Update frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx Co-authored-by: Simon Hornby <liquidwicked64@gmail.com> * Update frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx Co-authored-by: Simon Hornby <liquidwicked64@gmail.com> * address PR comments Co-authored-by: Simon Hornby <liquidwicked64@gmail.com>
This commit is contained in:
		
							parent
							
								
									8d6084de45
								
							
						
					
					
						commit
						d2324ee91f
					
				| @ -69,10 +69,10 @@ export const CreateGroup = () => { | |||||||
|         navigate(GO_BACK); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const isNameEmpty = (name: string) => name.length; |     const isNameNotEmpty = (name: string) => name.length; | ||||||
|     const isNameUnique = (name: string) => |     const isNameUnique = (name: string) => | ||||||
|         !groups?.filter(group => group.name === name).length; |         !groups?.filter(group => group.name === name).length; | ||||||
|     const isValid = isNameEmpty(name) && isNameUnique(name); |     const isValid = isNameNotEmpty(name) && isNameUnique(name); | ||||||
| 
 | 
 | ||||||
|     const onSetName = (name: string) => { |     const onSetName = (name: string) => { | ||||||
|         clearErrors(); |         clearErrors(); | ||||||
|  | |||||||
| @ -77,11 +77,11 @@ export const EditGroup = () => { | |||||||
|         navigate(GO_BACK); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const isNameEmpty = (name: string) => name.length; |     const isNameNotEmpty = (name: string) => name.length; | ||||||
|     const isNameUnique = (name: string) => |     const isNameUnique = (name: string) => | ||||||
|         !groups?.filter(group => group.name === name && group.id !== groupId) |         !groups?.filter(group => group.name === name && group.id !== groupId) | ||||||
|             .length; |             .length; | ||||||
|     const isValid = isNameEmpty(name) && isNameUnique(name); |     const isValid = isNameNotEmpty(name) && isNameUnique(name); | ||||||
| 
 | 
 | ||||||
|     const onSetName = (name: string) => { |     const onSetName = (name: string) => { | ||||||
|         clearErrors(); |         clearErrors(); | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import React, { FC } from 'react'; | import React, { FC } from 'react'; | ||||||
| import { Button, styled, Tooltip } from '@mui/material'; | import { Box, Button, styled } from '@mui/material'; | ||||||
| import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds'; | import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds'; | ||||||
| import Input from 'component/common/Input/Input'; | import Input from 'component/common/Input/Input'; | ||||||
| import { IGroupUser } from 'interfaces/group'; | import { IGroupUser } from 'interfaces/group'; | ||||||
| @ -9,8 +9,8 @@ import { GroupFormUsersTable } from './GroupFormUsersTable/GroupFormUsersTable'; | |||||||
| import { ItemList } from 'component/common/ItemList/ItemList'; | import { ItemList } from 'component/common/ItemList/ItemList'; | ||||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
| import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings'; | import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings'; | ||||||
| import { HelpOutline } from '@mui/icons-material'; |  | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
|  | import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; | ||||||
| 
 | 
 | ||||||
| const StyledForm = styled('form')(() => ({ | const StyledForm = styled('form')(() => ({ | ||||||
|     display: 'flex', |     display: 'flex', | ||||||
| @ -59,12 +59,6 @@ const StyledDescriptionBlock = styled('div')(({ theme }) => ({ | |||||||
|     borderRadius: theme.shape.borderRadiusMedium, |     borderRadius: theme.shape.borderRadiusMedium, | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const StyledHelpOutline = styled(HelpOutline)(({ theme }) => ({ |  | ||||||
|     fontSize: theme.fontSizes.smallBody, |  | ||||||
|     marginLeft: '0.3rem', |  | ||||||
|     color: theme.palette.grey[700], |  | ||||||
| })); |  | ||||||
| 
 |  | ||||||
| interface IGroupForm { | interface IGroupForm { | ||||||
|     name: string; |     name: string; | ||||||
|     description: string; |     description: string; | ||||||
| @ -155,17 +149,11 @@ export const GroupForm: FC<IGroupForm> = ({ | |||||||
|                             } |                             } | ||||||
|                             elseShow={() => ( |                             elseShow={() => ( | ||||||
|                                 <StyledDescriptionBlock> |                                 <StyledDescriptionBlock> | ||||||
|                                     <div> |                                     <Box sx={{ display: 'flex' }}> | ||||||
|                                         You can enable SSO groups syncronization |                                         You can enable SSO groups syncronization | ||||||
|                                         if needed |                                         if needed | ||||||
|                                         <Tooltip |                                         <HelpIcon tooltip="SSO groups syncronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized." /> | ||||||
|                                             title="You can enable SSO groups |                                     </Box> | ||||||
|                                             syncronization if needed" |  | ||||||
|                                             arrow |  | ||||||
|                                         > |  | ||||||
|                                             <StyledHelpOutline /> |  | ||||||
|                                         </Tooltip> |  | ||||||
|                                     </div> |  | ||||||
|                                     <Link data-loading to={`/admin/auth`}> |                                     <Link data-loading to={`/admin/auth`}> | ||||||
|                                         <span data-loading> |                                         <span data-loading> | ||||||
|                                             View SSO configuration |                                             View SSO configuration | ||||||
|  | |||||||
| @ -1,22 +0,0 @@ | |||||||
| import { makeStyles } from 'tss-react/mui'; |  | ||||||
| 
 |  | ||||||
| export const useStyles = makeStyles()(theme => ({ |  | ||||||
|     container: { |  | ||||||
|         display: 'inline-grid', |  | ||||||
|         alignItems: 'center', |  | ||||||
|         outline: 0, |  | ||||||
| 
 |  | ||||||
|         '&:is(:focus-visible, :active) > *, &:hover > *': { |  | ||||||
|             outlineStyle: 'solid', |  | ||||||
|             outlineWidth: 2, |  | ||||||
|             outlineOffset: 0, |  | ||||||
|             outlineColor: theme.palette.primary.main, |  | ||||||
|             borderRadius: '100%', |  | ||||||
|             color: theme.palette.primary.main, |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
|     icon: { |  | ||||||
|         fontSize: '1rem', |  | ||||||
|         color: theme.palette.inactiveIcon, |  | ||||||
|     }, |  | ||||||
| })); |  | ||||||
| @ -1,21 +1,38 @@ | |||||||
| import { Tooltip, TooltipProps } from '@mui/material'; | import { styled, Tooltip, TooltipProps } from '@mui/material'; | ||||||
| import { Info } from '@mui/icons-material'; | import { HelpOutline } from '@mui/icons-material'; | ||||||
| import { useStyles } from 'component/common/HelpIcon/HelpIcon.styles'; | 
 | ||||||
| import React from 'react'; | const StyledContainer = styled('span')(({ theme }) => ({ | ||||||
|  |     display: 'inline-grid', | ||||||
|  |     alignItems: 'center', | ||||||
|  |     outline: 0, | ||||||
|  |     cursor: 'pointer', | ||||||
|  |     '&:is(:focus-visible, :active) > *, &:hover > *': { | ||||||
|  |         outlineStyle: 'solid', | ||||||
|  |         outlineWidth: 2, | ||||||
|  |         outlineOffset: 0, | ||||||
|  |         outlineColor: theme.palette.primary.main, | ||||||
|  |         borderRadius: '100%', | ||||||
|  |         color: theme.palette.primary.main, | ||||||
|  |     }, | ||||||
|  |     '& svg': { | ||||||
|  |         fontSize: theme.fontSizes.mainHeader, | ||||||
|  |         color: theme.palette.neutral.main, | ||||||
|  |         marginLeft: theme.spacing(0.5), | ||||||
|  |     }, | ||||||
|  | })); | ||||||
| 
 | 
 | ||||||
| interface IHelpIconProps { | interface IHelpIconProps { | ||||||
|     tooltip: string; |     tooltip: string; | ||||||
|     placement?: TooltipProps['placement']; |     placement?: TooltipProps['placement']; | ||||||
|  |     children?: React.ReactNode; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const HelpIcon = ({ tooltip, placement }: IHelpIconProps) => { | export const HelpIcon = ({ tooltip, placement, children }: IHelpIconProps) => { | ||||||
|     const { classes: styles } = useStyles(); |  | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|         <Tooltip title={tooltip} placement={placement} arrow> |         <Tooltip title={tooltip} placement={placement} arrow> | ||||||
|             <span className={styles.container} tabIndex={0} aria-label="Help"> |             <StyledContainer tabIndex={0} aria-label="Help"> | ||||||
|                 <Info className={styles.icon} /> |                 {children ?? <HelpOutline />} | ||||||
|             </span> |             </StyledContainer> | ||||||
|         </Tooltip> |         </Tooltip> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -16,13 +16,14 @@ import { ADMIN } from 'component/providers/AccessProvider/permissions'; | |||||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { GO_BACK } from 'constants/navigate'; | import { GO_BACK } from 'constants/navigate'; | ||||||
|  | import { ENV_LIMIT } from 'constants/values'; | ||||||
| 
 | 
 | ||||||
| const CreateEnvironment = () => { | const CreateEnvironment = () => { | ||||||
|     const { setToastApiError, setToastData } = useToast(); |     const { setToastApiError, setToastData } = useToast(); | ||||||
|     const { uiConfig } = useUiConfig(); |     const { uiConfig } = useUiConfig(); | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const { environments } = useEnvironments(); |     const { environments } = useEnvironments(); | ||||||
|     const canCreateMoreEnvs = environments.length < 7; |     const canCreateMoreEnvs = environments.length < ENV_LIMIT; | ||||||
|     const { createEnvironment, loading } = useEnvironmentApi(); |     const { createEnvironment, loading } = useEnvironmentApi(); | ||||||
|     const { refetch } = useProjectRolePermissions(); |     const { refetch } = useProjectRolePermissions(); | ||||||
|     const { |     const { | ||||||
| @ -114,8 +115,9 @@ const CreateEnvironment = () => { | |||||||
|                     > |                     > | ||||||
|                         <Alert severity="error"> |                         <Alert severity="error"> | ||||||
|                             <p> |                             <p> | ||||||
|                                 Currently Unleash does not support more than 7 |                                 Currently Unleash does not support more than{' '} | ||||||
|                                 environments. If you need more please reach out. |                                 {ENV_LIMIT} environments. If you need more | ||||||
|  |                                 please reach out. | ||||||
|                             </p> |                             </p> | ||||||
|                         </Alert> |                         </Alert> | ||||||
|                         <br /> |                         <br /> | ||||||
|  | |||||||
| @ -12,6 +12,10 @@ import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironmen | |||||||
| import useToast from 'hooks/useToast'; | import useToast from 'hooks/useToast'; | ||||||
| import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch'; | import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch'; | ||||||
| import { EnvironmentActionCellPopover } from './EnvironmentActionCellPopover/EnvironmentActionCellPopover'; | import { EnvironmentActionCellPopover } from './EnvironmentActionCellPopover/EnvironmentActionCellPopover'; | ||||||
|  | import { EnvironmentCloneModal } from './EnvironmentCloneModal/EnvironmentCloneModal'; | ||||||
|  | import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; | ||||||
|  | import { EnvironmentTokenDialog } from './EnvironmentTokenDialog/EnvironmentTokenDialog'; | ||||||
|  | import { ENV_LIMIT } from 'constants/values'; | ||||||
| 
 | 
 | ||||||
| interface IEnvironmentTableActionsProps { | interface IEnvironmentTableActionsProps { | ||||||
|     environment: IEnvironment; |     environment: IEnvironment; | ||||||
| @ -22,13 +26,16 @@ export const EnvironmentActionCell = ({ | |||||||
| }: IEnvironmentTableActionsProps) => { | }: IEnvironmentTableActionsProps) => { | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|     const { setToastApiError, setToastData } = useToast(); |     const { setToastApiError, setToastData } = useToast(); | ||||||
|     const { refetchEnvironments } = useEnvironments(); |     const { environments, refetchEnvironments } = useEnvironments(); | ||||||
|     const { refetch: refetchPermissions } = useProjectRolePermissions(); |     const { refetch: refetchPermissions } = useProjectRolePermissions(); | ||||||
|     const { deleteEnvironment, toggleEnvironmentOn, toggleEnvironmentOff } = |     const { deleteEnvironment, toggleEnvironmentOn, toggleEnvironmentOff } = | ||||||
|         useEnvironmentApi(); |         useEnvironmentApi(); | ||||||
| 
 | 
 | ||||||
|     const [deleteModal, setDeleteModal] = useState(false); |     const [deleteModal, setDeleteModal] = useState(false); | ||||||
|     const [toggleModal, setToggleModal] = useState(false); |     const [toggleModal, setToggleModal] = useState(false); | ||||||
|  |     const [cloneModal, setCloneModal] = useState(false); | ||||||
|  |     const [tokenModal, setTokenModal] = useState(false); | ||||||
|  |     const [newToken, setNewToken] = useState<IApiToken>(); | ||||||
|     const [confirmName, setConfirmName] = useState(''); |     const [confirmName, setConfirmName] = useState(''); | ||||||
| 
 | 
 | ||||||
|     const handleDeleteEnvironment = async () => { |     const handleDeleteEnvironment = async () => { | ||||||
| @ -102,7 +109,17 @@ export const EnvironmentActionCell = ({ | |||||||
|             <EnvironmentActionCellPopover |             <EnvironmentActionCellPopover | ||||||
|                 environment={environment} |                 environment={environment} | ||||||
|                 onEdit={() => navigate(`/environments/${environment.name}`)} |                 onEdit={() => navigate(`/environments/${environment.name}`)} | ||||||
|                 onClone={() => console.log('TODO: CLONE')} |                 onClone={() => { | ||||||
|  |                     if (environments.length < ENV_LIMIT) { | ||||||
|  |                         setCloneModal(true); | ||||||
|  |                     } else { | ||||||
|  |                         setToastData({ | ||||||
|  |                             type: 'error', | ||||||
|  |                             title: 'Environment limit reached', | ||||||
|  |                             text: `You have reached the maximum number of environments (${ENV_LIMIT}). Please reach out if you need more.`, | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 }} | ||||||
|                 onDelete={() => setDeleteModal(true)} |                 onDelete={() => setDeleteModal(true)} | ||||||
|             /> |             /> | ||||||
|             <EnvironmentDeleteConfirm |             <EnvironmentDeleteConfirm | ||||||
| @ -119,6 +136,20 @@ export const EnvironmentActionCell = ({ | |||||||
|                 setToggleDialog={setToggleModal} |                 setToggleDialog={setToggleModal} | ||||||
|                 handleConfirmToggleEnvironment={handleConfirmToggleEnvironment} |                 handleConfirmToggleEnvironment={handleConfirmToggleEnvironment} | ||||||
|             /> |             /> | ||||||
|  |             <EnvironmentCloneModal | ||||||
|  |                 environment={environment} | ||||||
|  |                 open={cloneModal} | ||||||
|  |                 setOpen={setCloneModal} | ||||||
|  |                 newToken={(token: IApiToken) => { | ||||||
|  |                     setNewToken(token); | ||||||
|  |                     setTokenModal(true); | ||||||
|  |                 }} | ||||||
|  |             /> | ||||||
|  |             <EnvironmentTokenDialog | ||||||
|  |                 open={tokenModal} | ||||||
|  |                 setOpen={setTokenModal} | ||||||
|  |                 token={newToken} | ||||||
|  |             /> | ||||||
|         </ActionCell> |         </ActionCell> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -0,0 +1,385 @@ | |||||||
|  | import { | ||||||
|  |     Button, | ||||||
|  |     FormControl, | ||||||
|  |     FormControlLabel, | ||||||
|  |     Link, | ||||||
|  |     Radio, | ||||||
|  |     RadioGroup, | ||||||
|  |     styled, | ||||||
|  |     Switch, | ||||||
|  | } from '@mui/material'; | ||||||
|  | import FormTemplate from 'component/common/FormTemplate/FormTemplate'; | ||||||
|  | import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; | ||||||
|  | import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
|  | import useToast from 'hooks/useToast'; | ||||||
|  | import { FormEvent, useEffect, useState } from 'react'; | ||||||
|  | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
|  | import Input from 'component/common/Input/Input'; | ||||||
|  | import { | ||||||
|  |     IEnvironment, | ||||||
|  |     IEnvironmentClonePayload, | ||||||
|  | } from 'interfaces/environments'; | ||||||
|  | import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; | ||||||
|  | import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||||
|  | import EnvironmentTypeSelector from 'component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector'; | ||||||
|  | import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; | ||||||
|  | import { EnvironmentProjectSelect } from './EnvironmentProjectSelect/EnvironmentProjectSelect'; | ||||||
|  | import { SelectProjectInput } from 'component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput'; | ||||||
|  | import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||||
|  | import useApiTokensApi, { | ||||||
|  |     IApiTokenCreate, | ||||||
|  | } from 'hooks/api/actions/useApiTokensApi/useApiTokensApi'; | ||||||
|  | import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; | ||||||
|  | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | 
 | ||||||
|  | const StyledForm = styled('form')(() => ({ | ||||||
|  |     display: 'flex', | ||||||
|  |     flexDirection: 'column', | ||||||
|  |     height: '100%', | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledInputDescription = styled('p')(({ theme }) => ({ | ||||||
|  |     display: 'flex', | ||||||
|  |     color: theme.palette.text.primary, | ||||||
|  |     marginBottom: theme.spacing(1), | ||||||
|  |     '&:not(:first-of-type)': { | ||||||
|  |         marginTop: theme.spacing(4), | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledInputSecondaryDescription = styled('p')(({ theme }) => ({ | ||||||
|  |     color: theme.palette.text.secondary, | ||||||
|  |     marginBottom: theme.spacing(1), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledInput = styled(Input)(({ theme }) => ({ | ||||||
|  |     width: '100%', | ||||||
|  |     maxWidth: theme.spacing(50), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledSecondaryContainer = styled('div')(({ theme }) => ({ | ||||||
|  |     padding: theme.spacing(3), | ||||||
|  |     backgroundColor: theme.palette.secondaryContainer, | ||||||
|  |     borderRadius: theme.shape.borderRadiusMedium, | ||||||
|  |     marginTop: theme.spacing(4), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledInlineContainer = styled('div')(({ theme }) => ({ | ||||||
|  |     padding: theme.spacing(0, 4), | ||||||
|  |     '& > p:not(:first-of-type)': { | ||||||
|  |         marginTop: theme.spacing(2), | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledButtonContainer = styled('div')(({ theme }) => ({ | ||||||
|  |     marginTop: 'auto', | ||||||
|  |     display: 'flex', | ||||||
|  |     justifyContent: 'flex-end', | ||||||
|  |     [theme.breakpoints.down('sm')]: { | ||||||
|  |         marginTop: theme.spacing(4), | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledCancelButton = styled(Button)(({ theme }) => ({ | ||||||
|  |     marginLeft: theme.spacing(3), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | enum APITokenGeneration { | ||||||
|  |     LATER = 'later', | ||||||
|  |     NOW = 'now', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | enum ErrorField { | ||||||
|  |     NAME = 'name', | ||||||
|  |     PROJECTS = 'projects', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface ICreatePersonalAPITokenErrors { | ||||||
|  |     [ErrorField.NAME]?: string; | ||||||
|  |     [ErrorField.PROJECTS]?: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface ICreatePersonalAPITokenProps { | ||||||
|  |     environment: IEnvironment; | ||||||
|  |     open: boolean; | ||||||
|  |     setOpen: React.Dispatch<React.SetStateAction<boolean>>; | ||||||
|  |     newToken: (token: IApiToken) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const EnvironmentCloneModal = ({ | ||||||
|  |     environment, | ||||||
|  |     open, | ||||||
|  |     setOpen, | ||||||
|  |     newToken, | ||||||
|  | }: ICreatePersonalAPITokenProps) => { | ||||||
|  |     const { environments, refetchEnvironments } = useEnvironments(); | ||||||
|  |     const { cloneEnvironment, loading } = useEnvironmentApi(); | ||||||
|  |     const { createToken } = useApiTokensApi(); | ||||||
|  |     const { projects: allProjects } = useProjects(); | ||||||
|  |     const { setToastData, setToastApiError } = useToast(); | ||||||
|  |     const { uiConfig } = useUiConfig(); | ||||||
|  | 
 | ||||||
|  |     const [name, setName] = useState(`${environment.name}_clone`); | ||||||
|  |     const [type, setType] = useState('development'); | ||||||
|  |     const [projects, setProjects] = useState<string[]>([]); | ||||||
|  |     const [tokenProjects, setTokenProjects] = useState<string[]>(['*']); | ||||||
|  |     const [clonePermissions, setClonePermissions] = useState(true); | ||||||
|  |     const [apiTokenGeneration, setApiTokenGeneration] = | ||||||
|  |         useState<APITokenGeneration>(APITokenGeneration.LATER); | ||||||
|  |     const [errors, setErrors] = useState<ICreatePersonalAPITokenErrors>({}); | ||||||
|  | 
 | ||||||
|  |     const clearError = (field: ErrorField) => { | ||||||
|  |         setErrors(errors => ({ ...errors, [field]: undefined })); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const setError = (field: ErrorField, error: string) => { | ||||||
|  |         setErrors(errors => ({ ...errors, [field]: error })); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         setName(getUniqueName(environment.name)); | ||||||
|  |         setType('development'); | ||||||
|  |         setProjects([]); | ||||||
|  |         setTokenProjects(['*']); | ||||||
|  |         setClonePermissions(true); | ||||||
|  |         setErrors({}); | ||||||
|  |     }, [environment]); | ||||||
|  | 
 | ||||||
|  |     const getUniqueName = (name: string) => { | ||||||
|  |         let uniqueName = `${name}_clone`; | ||||||
|  |         let number = 2; | ||||||
|  |         while (!isNameUnique(uniqueName)) { | ||||||
|  |             uniqueName = `${environment.name}_clone_${number}`; | ||||||
|  |             number++; | ||||||
|  |         } | ||||||
|  |         return uniqueName; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const getCloneEnvironmentPayload = (): IEnvironmentClonePayload => ({ | ||||||
|  |         name, | ||||||
|  |         type, | ||||||
|  |         projects, | ||||||
|  |         clonePermissions, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const getApiTokenCreatePayload = (): IApiTokenCreate => ({ | ||||||
|  |         username: `${name}_token`, | ||||||
|  |         type: 'CLIENT', | ||||||
|  |         environment: name, | ||||||
|  |         projects: tokenProjects, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { | ||||||
|  |         e.preventDefault(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await cloneEnvironment( | ||||||
|  |                 environment.name, | ||||||
|  |                 getCloneEnvironmentPayload() | ||||||
|  |             ); | ||||||
|  |             const response = await createToken(getApiTokenCreatePayload()); | ||||||
|  |             if (apiTokenGeneration === APITokenGeneration.NOW) { | ||||||
|  |                 const token = await response.json(); | ||||||
|  |                 newToken(token); | ||||||
|  |             } | ||||||
|  |             setToastData({ | ||||||
|  |                 title: 'Environment successfully cloned!', | ||||||
|  |                 type: 'success', | ||||||
|  |             }); | ||||||
|  |             refetchEnvironments(); | ||||||
|  |             setOpen(false); | ||||||
|  |         } catch (error: unknown) { | ||||||
|  |             setToastApiError(formatUnknownError(error)); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const formatApiCode = () => { | ||||||
|  |         return `curl --location --request POST '${ | ||||||
|  |             uiConfig.unleashUrl | ||||||
|  |         }/api/admin/environments/${environment.name}/clone' \\ | ||||||
|  |     --header 'Authorization: INSERT_API_KEY' \\ | ||||||
|  |     --header 'Content-Type: application/json' \\ | ||||||
|  |     --data-raw '${JSON.stringify(getCloneEnvironmentPayload(), undefined, 2)}'`;
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const isNameNotEmpty = (name: string) => name.length; | ||||||
|  |     const isNameUnique = (name: string) => | ||||||
|  |         !environments?.some(environment => environment.name === name); | ||||||
|  |     const isValid = | ||||||
|  |         isNameNotEmpty(name) && isNameUnique(name) && tokenProjects.length; | ||||||
|  | 
 | ||||||
|  |     const onSetName = (name: string) => { | ||||||
|  |         clearError(ErrorField.NAME); | ||||||
|  |         if (!isNameUnique(name)) { | ||||||
|  |             setError( | ||||||
|  |                 ErrorField.NAME, | ||||||
|  |                 'An environment with that name already exists.' | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         setName(name); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const selectableProjects = allProjects.map(project => ({ | ||||||
|  |         value: project.id, | ||||||
|  |         label: project.name, | ||||||
|  |     })); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <SidebarModal | ||||||
|  |             open={open} | ||||||
|  |             onClose={() => { | ||||||
|  |                 setOpen(false); | ||||||
|  |             }} | ||||||
|  |             label={`Clone ${environment.name} environment`} | ||||||
|  |         > | ||||||
|  |             <FormTemplate | ||||||
|  |                 loading={loading} | ||||||
|  |                 modal | ||||||
|  |                 title={`Clone ${environment.name} environment`} | ||||||
|  |                 description="Cloning an environment will clone all feature toggles and their configuration (activation strategies, segments, status, etc) into a new environment." | ||||||
|  |                 documentationLink="https://docs.getunleash.io/user_guide/environments#cloning-environments" | ||||||
|  |                 documentationLinkLabel="Cloning environments documentation" | ||||||
|  |                 formatApiCode={formatApiCode} | ||||||
|  |             > | ||||||
|  |                 <StyledForm onSubmit={handleSubmit}> | ||||||
|  |                     <div> | ||||||
|  |                         <StyledInputDescription> | ||||||
|  |                             What is your new environment name? (Can't be changed | ||||||
|  |                             later) | ||||||
|  |                         </StyledInputDescription> | ||||||
|  |                         <StyledInput | ||||||
|  |                             autoFocus | ||||||
|  |                             label="Environment name" | ||||||
|  |                             error={Boolean(errors.name)} | ||||||
|  |                             errorText={errors.name} | ||||||
|  |                             value={name} | ||||||
|  |                             onChange={e => onSetName(e.target.value)} | ||||||
|  |                             required | ||||||
|  |                         /> | ||||||
|  |                         <StyledInputDescription> | ||||||
|  |                             What type of environment do you want to create? | ||||||
|  |                         </StyledInputDescription> | ||||||
|  |                         <EnvironmentTypeSelector | ||||||
|  |                             onChange={e => setType(e.currentTarget.value)} | ||||||
|  |                             value={type} | ||||||
|  |                         /> | ||||||
|  |                         <StyledInputDescription> | ||||||
|  |                             Select which projects you want to clone the | ||||||
|  |                             environment configuration in? | ||||||
|  |                             <HelpIcon tooltip="The cloned environment will keep the feature toggle state for the selected projects, where it will be enabled by default." /> | ||||||
|  |                         </StyledInputDescription> | ||||||
|  |                         <EnvironmentProjectSelect | ||||||
|  |                             projects={projects} | ||||||
|  |                             setProjects={setProjects} | ||||||
|  |                         /> | ||||||
|  |                         <StyledInputDescription> | ||||||
|  |                             Keep the users permission for this environment? | ||||||
|  |                         </StyledInputDescription> | ||||||
|  |                         <StyledInputSecondaryDescription> | ||||||
|  |                             If you turn it off, the permission for this | ||||||
|  |                             environment across all projects and feature toggles | ||||||
|  |                             will remain only for admin and editor roles. | ||||||
|  |                         </StyledInputSecondaryDescription> | ||||||
|  |                         <FormControlLabel | ||||||
|  |                             label={clonePermissions ? 'Yes' : 'No'} | ||||||
|  |                             control={ | ||||||
|  |                                 <Switch | ||||||
|  |                                     onChange={e => | ||||||
|  |                                         setClonePermissions(e.target.checked) | ||||||
|  |                                     } | ||||||
|  |                                     checked={clonePermissions} | ||||||
|  |                                 /> | ||||||
|  |                             } | ||||||
|  |                         /> | ||||||
|  |                         <StyledSecondaryContainer> | ||||||
|  |                             <StyledInputDescription> | ||||||
|  |                                 API Token | ||||||
|  |                             </StyledInputDescription> | ||||||
|  |                             <StyledInputSecondaryDescription> | ||||||
|  |                                 In order to connect your SDKs to your newly | ||||||
|  |                                 cloned environment, you will also need an API | ||||||
|  |                                 token.{' '} | ||||||
|  |                                 <Link | ||||||
|  |                                     href="https://docs.getunleash.io/reference/api-tokens-and-client-keys" | ||||||
|  |                                     target="_blank" | ||||||
|  |                                 > | ||||||
|  |                                     Read more about API tokens | ||||||
|  |                                 </Link> | ||||||
|  |                                 . | ||||||
|  |                             </StyledInputSecondaryDescription> | ||||||
|  |                             <FormControl> | ||||||
|  |                                 <RadioGroup | ||||||
|  |                                     value={apiTokenGeneration} | ||||||
|  |                                     onChange={e => | ||||||
|  |                                         setApiTokenGeneration( | ||||||
|  |                                             e.target.value as APITokenGeneration | ||||||
|  |                                         ) | ||||||
|  |                                     } | ||||||
|  |                                     name="api-token-generation" | ||||||
|  |                                 > | ||||||
|  |                                     <FormControlLabel | ||||||
|  |                                         value={APITokenGeneration.LATER} | ||||||
|  |                                         control={<Radio />} | ||||||
|  |                                         label="Generate an API token later" | ||||||
|  |                                     /> | ||||||
|  |                                     <FormControlLabel | ||||||
|  |                                         value={APITokenGeneration.NOW} | ||||||
|  |                                         control={<Radio />} | ||||||
|  |                                         label="Generate an API token now" | ||||||
|  |                                     /> | ||||||
|  |                                 </RadioGroup> | ||||||
|  |                             </FormControl> | ||||||
|  |                             <ConditionallyRender | ||||||
|  |                                 condition={ | ||||||
|  |                                     apiTokenGeneration === | ||||||
|  |                                     APITokenGeneration.NOW | ||||||
|  |                                 } | ||||||
|  |                                 show={ | ||||||
|  |                                     <StyledInlineContainer> | ||||||
|  |                                         <StyledInputSecondaryDescription> | ||||||
|  |                                             A new Server-side SDK (CLIENT) API | ||||||
|  |                                             token will be generated for the | ||||||
|  |                                             cloned environment, so you can get | ||||||
|  |                                             started right away. | ||||||
|  |                                         </StyledInputSecondaryDescription> | ||||||
|  |                                         <StyledInputDescription> | ||||||
|  |                                             Which projects do you want this | ||||||
|  |                                             token to give access to? | ||||||
|  |                                         </StyledInputDescription> | ||||||
|  |                                         <SelectProjectInput | ||||||
|  |                                             options={selectableProjects} | ||||||
|  |                                             defaultValue={tokenProjects} | ||||||
|  |                                             onChange={setTokenProjects} | ||||||
|  |                                             error={errors.projects} | ||||||
|  |                                             onFocus={() => | ||||||
|  |                                                 clearError(ErrorField.PROJECTS) | ||||||
|  |                                             } | ||||||
|  |                                         /> | ||||||
|  |                                     </StyledInlineContainer> | ||||||
|  |                                 } | ||||||
|  |                             /> | ||||||
|  |                         </StyledSecondaryContainer> | ||||||
|  |                     </div> | ||||||
|  | 
 | ||||||
|  |                     <StyledButtonContainer> | ||||||
|  |                         <Button | ||||||
|  |                             type="submit" | ||||||
|  |                             variant="contained" | ||||||
|  |                             color="primary" | ||||||
|  |                             disabled={!isValid} | ||||||
|  |                         > | ||||||
|  |                             Clone environment | ||||||
|  |                         </Button> | ||||||
|  |                         <StyledCancelButton | ||||||
|  |                             onClick={() => { | ||||||
|  |                                 setOpen(false); | ||||||
|  |                             }} | ||||||
|  |                         > | ||||||
|  |                             Cancel | ||||||
|  |                         </StyledCancelButton> | ||||||
|  |                     </StyledButtonContainer> | ||||||
|  |                 </StyledForm> | ||||||
|  |             </FormTemplate> | ||||||
|  |         </SidebarModal> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,161 @@ | |||||||
|  | import { | ||||||
|  |     Autocomplete, | ||||||
|  |     AutocompleteRenderGroupParams, | ||||||
|  |     Checkbox, | ||||||
|  |     styled, | ||||||
|  |     TextField, | ||||||
|  | } from '@mui/material'; | ||||||
|  | import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; | ||||||
|  | import CheckBoxIcon from '@mui/icons-material/CheckBox'; | ||||||
|  | import { caseInsensitiveSearch } from 'utils/search'; | ||||||
|  | import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||||
|  | import { Fragment } from 'react'; | ||||||
|  | import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||||
|  | import { SelectAllButton } from 'component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectAllButton/SelectAllButton'; | ||||||
|  | 
 | ||||||
|  | const StyledOption = styled('div')(({ theme }) => ({ | ||||||
|  |     display: 'flex', | ||||||
|  |     flexDirection: 'column', | ||||||
|  |     color: theme.palette.text.secondary, | ||||||
|  |     '& > span:first-of-type': { | ||||||
|  |         color: theme.palette.text.primary, | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledTags = styled('div')(({ theme }) => ({ | ||||||
|  |     paddingLeft: theme.spacing(1), | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const StyledGroupFormUsersSelect = styled('div')(({ theme }) => ({ | ||||||
|  |     display: 'flex', | ||||||
|  |     marginBottom: theme.spacing(3), | ||||||
|  |     '& > div:first-of-type': { | ||||||
|  |         width: '100%', | ||||||
|  |         maxWidth: theme.spacing(50), | ||||||
|  |         marginRight: theme.spacing(1), | ||||||
|  |     }, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | const renderOption = ( | ||||||
|  |     props: React.HTMLAttributes<HTMLLIElement>, | ||||||
|  |     option: IProjectBase, | ||||||
|  |     selected: boolean | ||||||
|  | ) => ( | ||||||
|  |     <li {...props}> | ||||||
|  |         <Checkbox | ||||||
|  |             icon={<CheckBoxOutlineBlankIcon fontSize="small" />} | ||||||
|  |             checkedIcon={<CheckBoxIcon fontSize="small" />} | ||||||
|  |             style={{ marginRight: 8 }} | ||||||
|  |             checked={selected} | ||||||
|  |         /> | ||||||
|  |         <StyledOption> | ||||||
|  |             <span>{option.name}</span> | ||||||
|  |             <span>{option.description}</span> | ||||||
|  |         </StyledOption> | ||||||
|  |     </li> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const renderTags = (value: IProjectBase[]) => ( | ||||||
|  |     <StyledTags> | ||||||
|  |         {value.length > 1 ? `${value.length} projects selected` : value[0].name} | ||||||
|  |     </StyledTags> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | interface IProjectBase { | ||||||
|  |     id: string; | ||||||
|  |     name: string; | ||||||
|  |     description: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface IEnvironmentProjectSelectProps { | ||||||
|  |     projects: string[]; | ||||||
|  |     setProjects: React.Dispatch<React.SetStateAction<string[]>>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const EnvironmentProjectSelect = ({ | ||||||
|  |     projects, | ||||||
|  |     setProjects, | ||||||
|  | }: IEnvironmentProjectSelectProps) => { | ||||||
|  |     const { projects: projectsAll } = useProjects(); | ||||||
|  | 
 | ||||||
|  |     const projectOptions = projectsAll | ||||||
|  |         .sort((a, b) => a.name.localeCompare(b.name)) | ||||||
|  |         .map(({ id, name, description }) => ({ | ||||||
|  |             id, | ||||||
|  |             name, | ||||||
|  |             description, | ||||||
|  |         })) as IProjectBase[]; | ||||||
|  | 
 | ||||||
|  |     const selectedProjects = projectOptions.filter(({ id }) => | ||||||
|  |         projects.includes(id) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const isAllSelected = | ||||||
|  |         projects.length > 0 && projects.length === projectOptions.length; | ||||||
|  | 
 | ||||||
|  |     const onSelectAllClick = () => { | ||||||
|  |         const newProjects = isAllSelected | ||||||
|  |             ? [] | ||||||
|  |             : projectOptions.map(({ id }) => id); | ||||||
|  |         setProjects(newProjects); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const renderGroup = ({ key, children }: AutocompleteRenderGroupParams) => ( | ||||||
|  |         <Fragment key={key}> | ||||||
|  |             <ConditionallyRender | ||||||
|  |                 condition={projectOptions.length > 2} | ||||||
|  |                 show={ | ||||||
|  |                     <SelectAllButton | ||||||
|  |                         isAllSelected={isAllSelected} | ||||||
|  |                         onClick={onSelectAllClick} | ||||||
|  |                     /> | ||||||
|  |                 } | ||||||
|  |             /> | ||||||
|  |             {children} | ||||||
|  |         </Fragment> | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <StyledGroupFormUsersSelect> | ||||||
|  |             <Autocomplete | ||||||
|  |                 size="small" | ||||||
|  |                 multiple | ||||||
|  |                 limitTags={1} | ||||||
|  |                 openOnFocus | ||||||
|  |                 disableCloseOnSelect | ||||||
|  |                 value={selectedProjects} | ||||||
|  |                 onChange={(event, newValue, reason) => { | ||||||
|  |                     if ( | ||||||
|  |                         event.type === 'keydown' && | ||||||
|  |                         (event as React.KeyboardEvent).key === 'Backspace' && | ||||||
|  |                         reason === 'removeOption' | ||||||
|  |                     ) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     setProjects(newValue.map(({ id }) => id)); | ||||||
|  |                 }} | ||||||
|  |                 options={projectOptions} | ||||||
|  |                 renderOption={(props, option, { selected }) => | ||||||
|  |                     renderOption(props, option, selected) | ||||||
|  |                 } | ||||||
|  |                 filterOptions={(options, { inputValue }) => | ||||||
|  |                     options.filter( | ||||||
|  |                         ({ name, description }) => | ||||||
|  |                             caseInsensitiveSearch(inputValue, name) || | ||||||
|  |                             caseInsensitiveSearch(inputValue, description) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 isOptionEqualToValue={(option, value) => option.id === value.id} | ||||||
|  |                 getOptionLabel={option => | ||||||
|  |                     option.name || option.description || '' | ||||||
|  |                 } | ||||||
|  |                 renderInput={params => ( | ||||||
|  |                     <TextField {...params} label="Projects" /> | ||||||
|  |                 )} | ||||||
|  |                 renderTags={value => renderTags(value)} | ||||||
|  |                 groupBy={() => 'Select/Deselect all'} | ||||||
|  |                 renderGroup={renderGroup} | ||||||
|  |             /> | ||||||
|  |         </StyledGroupFormUsersSelect> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -0,0 +1,37 @@ | |||||||
|  | import { Typography } from '@mui/material'; | ||||||
|  | import { UserToken } from 'component/admin/apiToken/ConfirmToken/UserToken/UserToken'; | ||||||
|  | import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||||
|  | import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; | ||||||
|  | import { Link } from 'react-router-dom'; | ||||||
|  | 
 | ||||||
|  | interface IEnvironmentTokenDialogProps { | ||||||
|  |     open: boolean; | ||||||
|  |     setOpen: React.Dispatch<React.SetStateAction<boolean>>; | ||||||
|  |     token?: IApiToken; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const EnvironmentTokenDialog = ({ | ||||||
|  |     open, | ||||||
|  |     setOpen, | ||||||
|  |     token, | ||||||
|  | }: IEnvironmentTokenDialogProps) => ( | ||||||
|  |     <Dialogue | ||||||
|  |         open={open} | ||||||
|  |         secondaryButtonText="Close" | ||||||
|  |         onClose={(_, muiCloseReason?: string) => { | ||||||
|  |             if (!muiCloseReason) { | ||||||
|  |                 setOpen(false); | ||||||
|  |             } | ||||||
|  |         }} | ||||||
|  |         title="New API token created" | ||||||
|  |     > | ||||||
|  |         <Typography variant="body1"> | ||||||
|  |             Your new token has been created successfully. | ||||||
|  |         </Typography> | ||||||
|  |         <Typography variant="body1"> | ||||||
|  |             You can also find it as "<strong>{token?.username}</strong>" in the{' '} | ||||||
|  |             <Link to="/admin/api">API access page</Link>. | ||||||
|  |         </Typography> | ||||||
|  |         <UserToken token={token?.secret || ''} /> | ||||||
|  |     </Dialogue> | ||||||
|  | ); | ||||||
| @ -212,11 +212,11 @@ export const CreatePersonalAPIToken: FC<ICreatePersonalAPITokenProps> = ({ | |||||||
|     --data-raw '${JSON.stringify(getPersonalAPITokenPayload(), undefined, 2)}'`;
 |     --data-raw '${JSON.stringify(getPersonalAPITokenPayload(), undefined, 2)}'`;
 | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const isDescriptionEmpty = (description: string) => description.length; |     const isDescriptionNotEmpty = (description: string) => description.length; | ||||||
|     const isDescriptionUnique = (description: string) => |     const isDescriptionUnique = (description: string) => | ||||||
|         !tokens?.some(token => token.description === description); |         !tokens?.some(token => token.description === description); | ||||||
|     const isValid = |     const isValid = | ||||||
|         isDescriptionEmpty(description) && |         isDescriptionNotEmpty(description) && | ||||||
|         isDescriptionUnique(description) && |         isDescriptionUnique(description) && | ||||||
|         expiresAt > new Date(); |         expiresAt > new Date(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								frontend/src/constants/values.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/constants/values.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export const ENV_LIMIT = 15; | ||||||
| @ -3,6 +3,7 @@ import { | |||||||
|     ISortOrderPayload, |     ISortOrderPayload, | ||||||
|     IEnvironmentEditPayload, |     IEnvironmentEditPayload, | ||||||
|     IEnvironment, |     IEnvironment, | ||||||
|  |     IEnvironmentClonePayload, | ||||||
| } from 'interfaces/environments'; | } from 'interfaces/environments'; | ||||||
| import useAPI from '../useApi/useApi'; | import useAPI from '../useApi/useApi'; | ||||||
| 
 | 
 | ||||||
| @ -82,6 +83,20 @@ const useEnvironmentApi = () => { | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const cloneEnvironment = async ( | ||||||
|  |         name: string, | ||||||
|  |         payload: IEnvironmentClonePayload | ||||||
|  |     ) => { | ||||||
|  |         const path = `api/admin/environments/${name}/clone`; | ||||||
|  |         const req = createRequest( | ||||||
|  |             path, | ||||||
|  |             { method: 'POST', body: JSON.stringify(payload) }, | ||||||
|  |             'cloneEnvironment' | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return await makeRequest(req.caller, req.id); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     const changeSortOrder = async (payload: ISortOrderPayload) => { |     const changeSortOrder = async (payload: ISortOrderPayload) => { | ||||||
|         const path = `api/admin/environments/sort-order`; |         const path = `api/admin/environments/sort-order`; | ||||||
|         const req = createRequest( |         const req = createRequest( | ||||||
| @ -140,6 +155,7 @@ const useEnvironmentApi = () => { | |||||||
|         loading, |         loading, | ||||||
|         deleteEnvironment, |         deleteEnvironment, | ||||||
|         updateEnvironment, |         updateEnvironment, | ||||||
|  |         cloneEnvironment, | ||||||
|         changeSortOrder, |         changeSortOrder, | ||||||
|         toggleEnvironmentOff, |         toggleEnvironmentOff, | ||||||
|         toggleEnvironmentOn, |         toggleEnvironmentOn, | ||||||
|  | |||||||
| @ -22,6 +22,13 @@ export interface IEnvironmentEditPayload { | |||||||
|     type: string; |     type: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface IEnvironmentClonePayload { | ||||||
|  |     name: string; | ||||||
|  |     type: string; | ||||||
|  |     projects: string[]; | ||||||
|  |     clonePermissions: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface IEnvironmentResponse { | export interface IEnvironmentResponse { | ||||||
|     environments: IEnvironment[]; |     environments: IEnvironment[]; | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user