1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00

feat: create and edit environment required approvals (#9621)

This commit is contained in:
Mateusz Kwasniewski 2025-03-26 15:54:46 +01:00 committed by GitHub
parent 07a4106f48
commit 1bd328f4e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 168 additions and 11 deletions

View File

@ -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={

View File

@ -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}

View File

@ -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 (
<FormControl component='fieldset'>
<StyledRadioGroup
data-loading
value={value ? 'yes' : 'no'}
onChange={(event) => {
if (event.target.value === 'yes') {
onChange(1);
} else {
onChange(null);
}
}}
>
<StyledRadioButtonGroup>
<FormControlLabel
value='no'
label='No'
control={<Radio />}
/>
<FormControlLabel
value='yes'
label='Yes'
control={<Radio />}
/>
</StyledRadioButtonGroup>
</StyledRadioGroup>
{value ? (
<>
<StyledRequiredApprovals>
Required approvals
</StyledRequiredApprovals>
<GeneralSelect
label='Set required approvals for the current environment'
visuallyHideLabel
sx={{ width: '160px' }}
options={approvalOptions}
value={value ? String(value) : undefined}
onChange={(approvals) => onChange(Number(approvals))}
IconComponent={KeyboardArrowDownOutlined}
fullWidth
/>
</>
) : null}
</FormControl>
);
};

View File

@ -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<React.SetStateAction<string>>;
setType: React.Dispatch<React.SetStateAction<string>>;
setRequiredApprovals: React.Dispatch<React.SetStateAction<number | null>>;
validateEnvironmentName?: (e: any) => void;
handleSubmit: (e: any) => void;
handleCancel: () => void;
@ -67,14 +72,19 @@ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
handleCancel,
name,
type,
requiredApprovals,
setName,
setType,
setRequiredApprovals,
validateEnvironmentName,
errors,
mode,
clearErrors,
Limit,
}) => {
const globalChangeRequestConfigEnabled = useUiFlag(
'globalChangeRequestConfig',
);
return (
<StyledForm onSubmit={handleSubmit}>
<StyledFormHeader>Environment information</StyledFormHeader>
@ -102,6 +112,19 @@ const EnvironmentForm: React.FC<IEnvironmentForm> = ({
onChange={(e) => setType(e.currentTarget.value)}
value={type}
/>
{globalChangeRequestConfigEnabled ? (
<>
<StyledInputDescription sx={{ mt: 2 }}>
Would you like to pre-define change requests for
this environment?
</StyledInputDescription>
<ChangeRequestSelector
onChange={setRequiredApprovals}
value={requiredApprovals}
/>
</>
) : null}
</StyledContainer>
<LimitContainer>{Limit}</LimitContainer>

View File

@ -21,7 +21,7 @@ const StyledRadioButtonGroup = styled('div')({
flexDirection: 'column',
});
const EnvironmentTypeSelector = ({
export const EnvironmentTypeSelector = ({
onChange,
value,
}: IEnvironmentTypeSelectorProps) => {
@ -56,5 +56,3 @@ const EnvironmentTypeSelector = ({
</FormControl>
);
};
export default EnvironmentTypeSelector;

View File

@ -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';

View File

@ -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,

View File

@ -12,6 +12,7 @@ export interface IEnvironment {
apiTokenCount?: number;
enabledToggleCount?: number;
lastSeenAt: string;
requiredApprovals?: number | null;
}
export interface IProjectEnvironment extends IEnvironment {

View File

@ -92,6 +92,7 @@ export type UiFlags = {
edgeObservability?: boolean;
adminNavUI?: boolean;
tagTypeColor?: boolean;
globalChangeRequestConfig?: boolean;
};
export interface IVersionInfo {

View File

@ -21,7 +21,7 @@ interface IEnvironmentsTable {
sort_order: number;
enabled: boolean;
protected: boolean;
required_approvals?: number;
required_approvals?: number | null;
}
interface IEnvironmentsWithCountsTable extends IEnvironmentsTable {

View File

@ -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 {