1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Add environment types environment order (#8447)

This commit is contained in:
Tymoteusz Czech 2024-10-15 11:00:31 +02:00 committed by GitHub
parent 4167d772e9
commit f5a2a18ffc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 170 additions and 44 deletions

View File

@ -51,6 +51,7 @@ function GeneralSelect<T extends string = string>({
classes,
fullWidth,
visuallyHideLabel,
labelId,
...rest
}: IGeneralSelectProps<T>) {
const onSelectChange = (event: SelectChangeEvent) => {
@ -65,12 +66,15 @@ function GeneralSelect<T extends string = string>({
classes={classes}
fullWidth={fullWidth}
>
<InputLabel
sx={visuallyHideLabel ? visuallyHidden : null}
htmlFor={id}
>
{label}
</InputLabel>
{label ? (
<InputLabel
sx={visuallyHideLabel ? visuallyHidden : null}
htmlFor={id}
id={labelId}
>
{label}
</InputLabel>
) : null}
<Select
name={name}
disabled={disabled}
@ -87,6 +91,7 @@ function GeneralSelect<T extends string = string>({
},
}}
IconComponent={KeyboardArrowDownOutlined}
labelId={labelId}
{...rest}
>
{options.map((option) => (

View File

@ -8,6 +8,7 @@ import { useFormErrors } from 'hooks/useFormErrors';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useOrderEnvironmentApi } from 'hooks/api/actions/useOrderEnvironmentsApi/useOrderEnvironmentsApi';
import type { OrderEnvironmentsSchema } from 'openapi';
type OrderEnvironmentsProps = {};
@ -29,11 +30,14 @@ export const OrderEnvironments: FC<OrderEnvironmentsProps> = () => {
return null;
}
const onSubmit = async (environments: string[]) => {
const onSubmit = async (
environments: OrderEnvironmentsSchema['environments'],
) => {
let hasErrors = false;
environments.forEach((environment, index) => {
const field = `environment-${index}`;
if (environment.trim() === '') {
const environmentName = environment.name.trim();
if (environmentName === '') {
errors.setFormError(field, 'Environment name is required');
hasErrors = true;
} else {

View File

@ -35,7 +35,9 @@ describe('OrderEnvironmentsDialog Component', () => {
screen.getAllByLabelText(/environment \d+ name/i);
expect(environmentInputs).toHaveLength(1);
const selectButton = screen.getByRole('combobox');
const selectButton = screen.getByRole('combobox', {
name: /select the number of additional environments/i,
});
fireEvent.mouseDown(selectButton);
const option2 = screen.getByRole('option', { name: '2 environments' });
@ -75,7 +77,9 @@ describe('OrderEnvironmentsDialog Component', () => {
const onSubmitMock = vi.fn();
renderComponent({ onSubmit: onSubmitMock });
const selectButton = screen.getByRole('combobox');
const selectButton = screen.getByRole('combobox', {
name: /select the number of additional environments/i,
});
fireEvent.mouseDown(selectButton);
const option2 = screen.getByRole('option', { name: '2 environments' });
@ -97,7 +101,10 @@ describe('OrderEnvironmentsDialog Component', () => {
fireEvent.click(submitButton);
expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(['Dev', 'Staging']);
expect(onSubmitMock).toHaveBeenCalledWith([
{ name: 'Dev', type: 'development' },
{ name: 'Staging', type: 'development' },
]);
});
test('should call onClose when "Cancel" button is clicked', () => {
@ -114,7 +121,9 @@ describe('OrderEnvironmentsDialog Component', () => {
const onSubmitMock = vi.fn();
renderComponent({ onSubmit: onSubmitMock });
const selectButton = screen.getByRole('combobox');
const selectButton = screen.getByRole('combobox', {
name: /select the number of additional environments/i,
});
fireEvent.mouseDown(selectButton);
const option3 = screen.getByRole('option', { name: '3 environments' });
@ -146,6 +155,58 @@ describe('OrderEnvironmentsDialog Component', () => {
fireEvent.click(submitButton);
expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith(['Dev', 'Staging']);
expect(onSubmitMock).toHaveBeenCalledWith([
{ name: 'Dev', type: 'development' },
{ name: 'Prod', type: 'development' },
]);
});
test('should allow for changing environment types', () => {
const onSubmitMock = vi.fn();
renderComponent({ onSubmit: onSubmitMock });
const selectButton = screen.getByRole('combobox', {
name: /select the number of additional environments/i,
});
fireEvent.mouseDown(selectButton);
const option3 = screen.getByRole('option', { name: '2 environments' });
fireEvent.click(option3);
const checkbox = screen.getByRole('checkbox', {
name: /i understand adding environments leads to extra costs/i,
});
fireEvent.click(checkbox);
const environmentInputs =
screen.getAllByLabelText(/environment \d+ name/i);
fireEvent.change(environmentInputs[0], { target: { value: 'Test' } });
fireEvent.change(environmentInputs[1], {
target: { value: 'Staging' },
});
const typeSelects = screen.getAllByRole('combobox', {
name: /type of environment/i,
});
fireEvent.mouseDown(typeSelects[0]);
const optionTesting = screen.getByRole('option', {
name: /testing/i,
});
fireEvent.click(optionTesting);
fireEvent.mouseDown(typeSelects[1]);
const optionProduction = screen.getByRole('option', {
name: /pre\-production/i,
});
fireEvent.click(optionProduction);
const submitButton = screen.getByRole('button', { name: /order/i });
fireEvent.click(submitButton);
expect(onSubmitMock).toHaveBeenCalledTimes(1);
expect(onSubmitMock).toHaveBeenCalledWith([
{ name: 'Test', type: 'testing' },
{ name: 'Staging', type: 'pre-production' },
]);
});
});

View File

@ -6,18 +6,19 @@ import {
Dialog,
styled,
Typography,
TextField,
} from '@mui/material';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { OrderEnvironmentsDialogPricing } from './OrderEnvironmentsDialogPricing/OrderEnvironmentsDialogPricing';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import Input from 'component/common/Input/Input';
import type { IFormErrors } from 'hooks/useFormErrors';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { OrderEnvironmentsSchemaEnvironmentsItem } from 'openapi';
type OrderEnvironmentsDialogProps = {
open: boolean;
onClose: () => void;
onSubmit: (environments: string[]) => void;
onSubmit: (environments: OrderEnvironmentsSchemaEnvironmentsItem[]) => void;
errors?: IFormErrors;
};
@ -50,6 +51,16 @@ const StyledGeneralSelect = styled(GeneralSelect)(({ theme }) => ({
margin: theme.spacing(1, 0),
}));
const StyledTypeSelect = styled(GeneralSelect)(({ theme }) => ({
minWidth: '166px',
}));
const StyledEnvironmentInputs = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(2),
marginBottom: theme.spacing(2),
}));
const StyledFields = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
@ -72,6 +83,12 @@ const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
const PRICE = 10;
const OPTIONS = [1, 2, 3];
const ENVIRONMENT_TYPES = [
'development',
'testing',
'pre-production',
'production',
];
export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
open,
@ -82,7 +99,9 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
const { trackEvent } = usePlausibleTracker();
const [selectedOption, setSelectedOption] = useState(OPTIONS[0]);
const [costCheckboxChecked, setCostCheckboxChecked] = useState(false);
const [environmentNames, setEnvironmentNames] = useState<string[]>(['']);
const [environments, setEnvironments] = useState<
{ name: string; type: string }[]
>([{ name: '', type: ENVIRONMENT_TYPES[0] }]);
const trackEnvironmentSelect = () => {
trackEvent('order-environments', {
@ -110,7 +129,7 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
<Button
variant='contained'
disabled={!costCheckboxChecked}
onClick={() => onSubmit(environmentNames)}
onClick={() => onSubmit(environments)}
>
Order
</Button>
@ -129,14 +148,11 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
</Typography>
<StyledFields>
<Box>
<Typography
component='label'
htmlFor='numberOfEnvironments'
>
<Typography component='label' id='numberOfEnvironments'>
Select the number of additional environments
</Typography>
<StyledGeneralSelect
id='numberOfEnvironments'
labelId='numberOfEnvironments'
value={`${selectedOption}`}
options={OPTIONS.map((option) => ({
key: `${option}`,
@ -145,11 +161,14 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
onChange={(option) => {
const value = Number.parseInt(option, 10);
setSelectedOption(value);
setEnvironmentNames((names) =>
[...names, ...Array(value).fill('')].slice(
0,
value,
),
setEnvironments((envs) =>
[
...envs,
...Array(value).fill({
name: '',
type: ENVIRONMENT_TYPES[0],
}),
].slice(0, value),
);
trackEnvironmentSelect();
}}
@ -164,22 +183,45 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
const error = errors?.getFormError(
`environment-${i}`,
);
return (
<Input
key={i}
label={`Environment ${i + 1} name`}
value={environmentNames[i]}
onChange={(event) => {
setEnvironmentNames((names) => {
const newValues = [...names];
newValues[i] = event.target.value;
return newValues;
});
}}
error={Boolean(error)}
errorText={error}
/>
<StyledEnvironmentInputs key={i}>
<StyledTypeSelect
label='Type of environment'
labelId={`environmentType${i}`}
value={
environments[i]?.type ||
ENVIRONMENT_TYPES[0]
}
options={ENVIRONMENT_TYPES.map(
(type) => ({
key: type,
label: type,
}),
)}
onChange={(type) => {
const newEnvironments = [
...environments,
];
newEnvironments[i].type = type;
setEnvironments(newEnvironments);
}}
/>
<TextField
size='small'
label={`Environment ${i + 1} Name`}
value={environments[i]?.name || ''}
onChange={(e) => {
const newEnvironments = [
...environments,
];
newEnvironments[i].name =
e.target.value;
setEnvironments(newEnvironments);
}}
error={!!error}
helperText={error}
/>
</StyledEnvironmentInputs>
);
})}
</StyledEnvironmentNameInputs>

View File

@ -863,6 +863,7 @@ export * from './oidcSettingsSchemaOneOfFourDefaultRootRole';
export * from './oidcSettingsSchemaOneOfFourIdTokenSigningAlgorithm';
export * from './oidcSettingsSchemaOneOfIdTokenSigningAlgorithm';
export * from './orderEnvironmentsSchema';
export * from './orderEnvironmentsSchemaEnvironmentsItem';
export * from './outdatedSdksSchema';
export * from './outdatedSdksSchemaSdksItem';
export * from './overrideSchema';

View File

@ -3,11 +3,12 @@
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { OrderEnvironmentsSchemaEnvironmentsItem } from './orderEnvironmentsSchemaEnvironmentsItem';
/**
* A request for hosted customers to order new environments in Unleash.
*/
export interface OrderEnvironmentsSchema {
/** An array of environment names to be ordered. */
environments: string[];
/** An array of environments to be ordered, each with a name and type. */
environments: OrderEnvironmentsSchemaEnvironmentsItem[];
}

View File

@ -0,0 +1,12 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
export type OrderEnvironmentsSchemaEnvironmentsItem = {
/** The name of the environment. */
name: string;
/** The type of the environment. */
type: string;
};