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/ProjectSelector/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.background.elevation2, 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 IEnvironmentCloneModalErrors { [ErrorField.NAME]?: string; [ErrorField.PROJECTS]?: string; } interface IEnvironmentCloneModalProps { environment: IEnvironment; open: boolean; setOpen: React.Dispatch>; newToken: (token: IApiToken) => void; } export const EnvironmentCloneModal = ({ environment, open, setOpen, newToken, }: IEnvironmentCloneModalProps) => { 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([]); const [tokenProjects, setTokenProjects] = useState(['*']); const [clonePermissions, setClonePermissions] = useState(true); const [apiTokenGeneration, setApiTokenGeneration] = useState(APITokenGeneration.LATER); const [errors, setErrors] = useState({}); 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); setApiTokenGeneration(APITokenGeneration.LATER); 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) => { e.preventDefault(); try { await cloneEnvironment( environment.name, getCloneEnvironmentPayload() ); if (apiTokenGeneration === APITokenGeneration.NOW) { const response = await createToken(getApiTokenCreatePayload()); 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 ( { setOpen(false); }} label={`Clone ${environment.name} environment`} >
What is your new environment name? (Can't be changed later) onSetName(e.target.value)} required /> What type of environment do you want to create? setType(e.currentTarget.value)} value={type} /> Select which projects you want to clone the environment configuration in? Keep the users permission for this environment? If you turn it off, the permission for this environment across all projects and feature toggles will remain only for admin and editor roles. setClonePermissions(e.target.checked) } checked={clonePermissions} /> } /> API Token In order to connect your SDKs to your newly cloned environment, you will also need an API token.{' '} Read more about API tokens . setApiTokenGeneration( e.target.value as APITokenGeneration ) } name="api-token-generation" > } label="I want to generate an API token later" /> } label="Generate an API token now" /> A new Server-side SDK (CLIENT) API token will be generated for the cloned environment, so you can get started right away. Which projects do you want this token to give access to? clearError( ErrorField.PROJECTS ) } /> } />
{ setOpen(false); }} > Cancel
); };