diff --git a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx index 4b8912506a..f438cd063c 100644 --- a/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx +++ b/frontend/src/component/environments/CreateEnvironment/CreateEnvironment.tsx @@ -27,6 +27,8 @@ const CreateEnvironment = () => { setName, type, setType, + requiredApprovals, + setRequiredApprovals, getEnvPayload, validateEnvironmentName, clearErrors, @@ -91,6 +93,8 @@ const CreateEnvironment = () => { type={type} setName={setName} setType={setType} + requiredApprovals={requiredApprovals} + setRequiredApprovals={setRequiredApprovals} mode='Create' clearErrors={clearErrors} Limit={ diff --git a/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx b/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx index 7ad42f9126..00bc1ba36b 100644 --- a/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx +++ b/frontend/src/component/environments/EditEnvironment/EditEnvironment.tsx @@ -12,6 +12,7 @@ import useEnvironmentForm from '../hooks/useEnvironmentForm'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { GO_BACK } from 'constants/navigate'; +import { useUiFlag } from 'hooks/useUiFlag'; const EditEnvironment = () => { const { uiConfig } = useUiConfig(); @@ -21,14 +22,30 @@ const EditEnvironment = () => { const { updateEnvironment } = useEnvironmentApi(); const navigate = useNavigate(); - const { name, type, setName, setType, errors, clearErrors } = - useEnvironmentForm(environment.name, environment.type); + const { + name, + type, + setName, + setType, + requiredApprovals, + setRequiredApprovals, + errors, + clearErrors, + } = useEnvironmentForm( + environment.name, + environment.type, + environment.requiredApprovals, + ); const { refetch } = usePermissions(); + const globalChangeRequestConfigEnabled = useUiFlag( + 'globalChangeRequestConfig', + ); const editPayload = () => { return { type, sortOrder: environment.sortOrder, + ...(globalChangeRequestConfigEnabled ? { requiredApprovals } : {}), }; }; @@ -84,6 +101,8 @@ const EditEnvironment = () => { type={type} setName={setName} setType={setType} + requiredApprovals={requiredApprovals} + setRequiredApprovals={setRequiredApprovals} mode='Edit' errors={errors} clearErrors={clearErrors} diff --git a/frontend/src/component/environments/EnvironmentForm/ChangeRequestSelector.tsx b/frontend/src/component/environments/EnvironmentForm/ChangeRequestSelector.tsx new file mode 100644 index 0000000000..59aa76e330 --- /dev/null +++ b/frontend/src/component/environments/EnvironmentForm/ChangeRequestSelector.tsx @@ -0,0 +1,97 @@ +import { + FormControl, + FormControlLabel, + Radio, + RadioGroup, + styled, +} from '@mui/material'; +import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined'; +import GeneralSelect from '../../common/GeneralSelect/GeneralSelect'; +import { useTheme } from '@mui/material/styles'; + +interface IEnvironmentChangeRequestProps { + onChange: (approvals: number | null) => void; + value: number | null; +} + +const StyledRadioGroup = styled(RadioGroup)({ + flexDirection: 'row', +}); + +const StyledRadioButtonGroup = styled('div')({ + display: 'flex', + flexDirection: 'column', +}); + +const StyledRequiredApprovals = styled('p')(({ theme }) => ({ + marginTop: theme.spacing(1), + marginBottom: theme.spacing(0.5), +})); + +const useApprovalOptions = () => { + const theme = useTheme(); + const approvalOptions = Array.from(Array(10).keys()) + .map((key) => String(key + 1)) + .map((key) => { + const labelText = key === '1' ? 'approval' : 'approvals'; + return { + key, + label: `${key} ${labelText}`, + sx: { fontSize: theme.fontSizes.smallBody }, + }; + }); + return approvalOptions; +}; + +export const ChangeRequestSelector = ({ + onChange, + value, +}: IEnvironmentChangeRequestProps) => { + const approvalOptions = useApprovalOptions(); + + return ( + + { + if (event.target.value === 'yes') { + onChange(1); + } else { + onChange(null); + } + }} + > + + } + /> + } + /> + + + {value ? ( + <> + + Required approvals + + onChange(Number(approvals))} + IconComponent={KeyboardArrowDownOutlined} + fullWidth + /> + + ) : null} + + ); +}; diff --git a/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx index 95fa0b6fd2..fae592d9ff 100644 --- a/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentForm.tsx @@ -1,14 +1,19 @@ import { Box, Button, styled } from '@mui/material'; import type React from 'react'; import Input from 'component/common/Input/Input'; -import EnvironmentTypeSelector from './EnvironmentTypeSelector/EnvironmentTypeSelector'; +import { EnvironmentTypeSelector } from './EnvironmentTypeSelector'; +import { ChangeRequestSelector } from './ChangeRequestSelector'; import { trim } from 'component/common/util'; +import { useUiFlag } from '../../../hooks/useUiFlag'; interface IEnvironmentForm { name: string; type: string; + requiredApprovals: number | null; setName: React.Dispatch>; setType: React.Dispatch>; + setRequiredApprovals: React.Dispatch>; + validateEnvironmentName?: (e: any) => void; handleSubmit: (e: any) => void; handleCancel: () => void; @@ -67,14 +72,19 @@ const EnvironmentForm: React.FC = ({ handleCancel, name, type, + requiredApprovals, setName, setType, + setRequiredApprovals, validateEnvironmentName, errors, mode, clearErrors, Limit, }) => { + const globalChangeRequestConfigEnabled = useUiFlag( + 'globalChangeRequestConfig', + ); return ( Environment information @@ -102,6 +112,19 @@ const EnvironmentForm: React.FC = ({ onChange={(e) => setType(e.currentTarget.value)} value={type} /> + + {globalChangeRequestConfigEnabled ? ( + <> + + Would you like to pre-define change requests for + this environment? + + + + ) : null} {Limit} diff --git a/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector.tsx similarity index 95% rename from frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx rename to frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector.tsx index 60f73f2c8c..02d73b03b3 100644 --- a/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector/EnvironmentTypeSelector.tsx +++ b/frontend/src/component/environments/EnvironmentForm/EnvironmentTypeSelector.tsx @@ -21,7 +21,7 @@ const StyledRadioButtonGroup = styled('div')({ flexDirection: 'column', }); -const EnvironmentTypeSelector = ({ +export const EnvironmentTypeSelector = ({ onChange, value, }: IEnvironmentTypeSelectorProps) => { @@ -56,5 +56,3 @@ const EnvironmentTypeSelector = ({ ); }; - -export default EnvironmentTypeSelector; diff --git a/frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx b/frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx index 6f7da59ace..ee3dfc90f7 100644 --- a/frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx +++ b/frontend/src/component/environments/EnvironmentTable/EnvironmentActionCell/EnvironmentCloneModal/EnvironmentCloneModal.tsx @@ -21,7 +21,7 @@ import type { } 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 { EnvironmentTypeSelector } from 'component/environments/EnvironmentForm/EnvironmentTypeSelector'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; import { EnvironmentProjectSelect } from './EnvironmentProjectSelect/EnvironmentProjectSelect'; import { SelectProjectInput } from 'component/admin/apiToken/ApiTokenForm/ProjectSelector/SelectProjectInput/SelectProjectInput'; diff --git a/frontend/src/component/environments/hooks/useEnvironmentForm.ts b/frontend/src/component/environments/hooks/useEnvironmentForm.ts index a8da3299df..790518705b 100644 --- a/frontend/src/component/environments/hooks/useEnvironmentForm.ts +++ b/frontend/src/component/environments/hooks/useEnvironmentForm.ts @@ -2,9 +2,16 @@ import { useEffect, useState } from 'react'; import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import { formatUnknownError } from 'utils/formatUnknownError'; -const useEnvironmentForm = (initialName = '', initialType = 'development') => { +const useEnvironmentForm = ( + initialName = '', + initialType = 'development', + initialRequiredApprovals: number | null = null, +) => { const [name, setName] = useState(initialName); const [type, setType] = useState(initialType); + const [requiredApprovals, setRequiredApprovals] = useState( + initialRequiredApprovals, + ); const [errors, setErrors] = useState({}); useEffect(() => { @@ -15,12 +22,17 @@ const useEnvironmentForm = (initialName = '', initialType = 'development') => { setType(initialType); }, [initialType]); + useEffect(() => { + setRequiredApprovals(initialRequiredApprovals); + }, [initialRequiredApprovals]); + const { validateEnvName } = useEnvironmentApi(); const getEnvPayload = () => { return { name, type, + ...(requiredApprovals ? { requiredApprovals } : {}), }; }; @@ -51,6 +63,8 @@ const useEnvironmentForm = (initialName = '', initialType = 'development') => { setName, type, setType, + requiredApprovals, + setRequiredApprovals, getEnvPayload, validateEnvironmentName, clearErrors, diff --git a/frontend/src/interfaces/environments.ts b/frontend/src/interfaces/environments.ts index 37e535b481..b5f9fcd55f 100644 --- a/frontend/src/interfaces/environments.ts +++ b/frontend/src/interfaces/environments.ts @@ -12,6 +12,7 @@ export interface IEnvironment { apiTokenCount?: number; enabledToggleCount?: number; lastSeenAt: string; + requiredApprovals?: number | null; } export interface IProjectEnvironment extends IEnvironment { diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index ac8c727705..4560c28f32 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -92,6 +92,7 @@ export type UiFlags = { edgeObservability?: boolean; adminNavUI?: boolean; tagTypeColor?: boolean; + globalChangeRequestConfig?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/features/project-environments/environment-store.ts b/src/lib/features/project-environments/environment-store.ts index 4559fcec7d..28dc2b089e 100644 --- a/src/lib/features/project-environments/environment-store.ts +++ b/src/lib/features/project-environments/environment-store.ts @@ -21,7 +21,7 @@ interface IEnvironmentsTable { sort_order: number; enabled: boolean; protected: boolean; - required_approvals?: number; + required_approvals?: number | null; } interface IEnvironmentsWithCountsTable extends IEnvironmentsTable { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 82207be1a6..625d37422b 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -198,7 +198,7 @@ export interface IEnvironment { projectCount?: number; apiTokenCount?: number; enabledToggleCount?: number; - requiredApprovals?: number; + requiredApprovals?: number | null; } export interface IProjectEnvironment extends IEnvironment { @@ -216,7 +216,7 @@ export interface IEnvironmentCreate { type: string; sortOrder?: number; enabled?: boolean; - requiredApprovals?: number; + requiredApprovals?: number | null; } export interface IEnvironmentClone {