1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-28 17:55:15 +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, classes,
fullWidth, fullWidth,
visuallyHideLabel, visuallyHideLabel,
labelId,
...rest ...rest
}: IGeneralSelectProps<T>) { }: IGeneralSelectProps<T>) {
const onSelectChange = (event: SelectChangeEvent) => { const onSelectChange = (event: SelectChangeEvent) => {
@ -65,12 +66,15 @@ function GeneralSelect<T extends string = string>({
classes={classes} classes={classes}
fullWidth={fullWidth} fullWidth={fullWidth}
> >
{label ? (
<InputLabel <InputLabel
sx={visuallyHideLabel ? visuallyHidden : null} sx={visuallyHideLabel ? visuallyHidden : null}
htmlFor={id} htmlFor={id}
id={labelId}
> >
{label} {label}
</InputLabel> </InputLabel>
) : null}
<Select <Select
name={name} name={name}
disabled={disabled} disabled={disabled}
@ -87,6 +91,7 @@ function GeneralSelect<T extends string = string>({
}, },
}} }}
IconComponent={KeyboardArrowDownOutlined} IconComponent={KeyboardArrowDownOutlined}
labelId={labelId}
{...rest} {...rest}
> >
{options.map((option) => ( {options.map((option) => (

View File

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

View File

@ -35,7 +35,9 @@ describe('OrderEnvironmentsDialog Component', () => {
screen.getAllByLabelText(/environment \d+ name/i); screen.getAllByLabelText(/environment \d+ name/i);
expect(environmentInputs).toHaveLength(1); 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); fireEvent.mouseDown(selectButton);
const option2 = screen.getByRole('option', { name: '2 environments' }); const option2 = screen.getByRole('option', { name: '2 environments' });
@ -75,7 +77,9 @@ describe('OrderEnvironmentsDialog Component', () => {
const onSubmitMock = vi.fn(); const onSubmitMock = vi.fn();
renderComponent({ onSubmit: onSubmitMock }); renderComponent({ onSubmit: onSubmitMock });
const selectButton = screen.getByRole('combobox'); const selectButton = screen.getByRole('combobox', {
name: /select the number of additional environments/i,
});
fireEvent.mouseDown(selectButton); fireEvent.mouseDown(selectButton);
const option2 = screen.getByRole('option', { name: '2 environments' }); const option2 = screen.getByRole('option', { name: '2 environments' });
@ -97,7 +101,10 @@ describe('OrderEnvironmentsDialog Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(onSubmitMock).toHaveBeenCalledTimes(1); 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', () => { test('should call onClose when "Cancel" button is clicked', () => {
@ -114,7 +121,9 @@ describe('OrderEnvironmentsDialog Component', () => {
const onSubmitMock = vi.fn(); const onSubmitMock = vi.fn();
renderComponent({ onSubmit: onSubmitMock }); renderComponent({ onSubmit: onSubmitMock });
const selectButton = screen.getByRole('combobox'); const selectButton = screen.getByRole('combobox', {
name: /select the number of additional environments/i,
});
fireEvent.mouseDown(selectButton); fireEvent.mouseDown(selectButton);
const option3 = screen.getByRole('option', { name: '3 environments' }); const option3 = screen.getByRole('option', { name: '3 environments' });
@ -146,6 +155,58 @@ describe('OrderEnvironmentsDialog Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(onSubmitMock).toHaveBeenCalledTimes(1); 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, Dialog,
styled, styled,
Typography, Typography,
TextField,
} from '@mui/material'; } from '@mui/material';
import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { OrderEnvironmentsDialogPricing } from './OrderEnvironmentsDialogPricing/OrderEnvironmentsDialogPricing'; import { OrderEnvironmentsDialogPricing } from './OrderEnvironmentsDialogPricing/OrderEnvironmentsDialogPricing';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import Input from 'component/common/Input/Input';
import type { IFormErrors } from 'hooks/useFormErrors'; import type { IFormErrors } from 'hooks/useFormErrors';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { OrderEnvironmentsSchemaEnvironmentsItem } from 'openapi';
type OrderEnvironmentsDialogProps = { type OrderEnvironmentsDialogProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (environments: string[]) => void; onSubmit: (environments: OrderEnvironmentsSchemaEnvironmentsItem[]) => void;
errors?: IFormErrors; errors?: IFormErrors;
}; };
@ -50,6 +51,16 @@ const StyledGeneralSelect = styled(GeneralSelect)(({ theme }) => ({
margin: theme.spacing(1, 0), 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 }) => ({ const StyledFields = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -72,6 +83,12 @@ const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
const PRICE = 10; const PRICE = 10;
const OPTIONS = [1, 2, 3]; const OPTIONS = [1, 2, 3];
const ENVIRONMENT_TYPES = [
'development',
'testing',
'pre-production',
'production',
];
export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
open, open,
@ -82,7 +99,9 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const [selectedOption, setSelectedOption] = useState(OPTIONS[0]); const [selectedOption, setSelectedOption] = useState(OPTIONS[0]);
const [costCheckboxChecked, setCostCheckboxChecked] = useState(false); 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 = () => { const trackEnvironmentSelect = () => {
trackEvent('order-environments', { trackEvent('order-environments', {
@ -110,7 +129,7 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
<Button <Button
variant='contained' variant='contained'
disabled={!costCheckboxChecked} disabled={!costCheckboxChecked}
onClick={() => onSubmit(environmentNames)} onClick={() => onSubmit(environments)}
> >
Order Order
</Button> </Button>
@ -129,14 +148,11 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
</Typography> </Typography>
<StyledFields> <StyledFields>
<Box> <Box>
<Typography <Typography component='label' id='numberOfEnvironments'>
component='label'
htmlFor='numberOfEnvironments'
>
Select the number of additional environments Select the number of additional environments
</Typography> </Typography>
<StyledGeneralSelect <StyledGeneralSelect
id='numberOfEnvironments' labelId='numberOfEnvironments'
value={`${selectedOption}`} value={`${selectedOption}`}
options={OPTIONS.map((option) => ({ options={OPTIONS.map((option) => ({
key: `${option}`, key: `${option}`,
@ -145,11 +161,14 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
onChange={(option) => { onChange={(option) => {
const value = Number.parseInt(option, 10); const value = Number.parseInt(option, 10);
setSelectedOption(value); setSelectedOption(value);
setEnvironmentNames((names) => setEnvironments((envs) =>
[...names, ...Array(value).fill('')].slice( [
0, ...envs,
value, ...Array(value).fill({
), name: '',
type: ENVIRONMENT_TYPES[0],
}),
].slice(0, value),
); );
trackEnvironmentSelect(); trackEnvironmentSelect();
}} }}
@ -164,22 +183,45 @@ export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
const error = errors?.getFormError( const error = errors?.getFormError(
`environment-${i}`, `environment-${i}`,
); );
return ( return (
<Input <StyledEnvironmentInputs key={i}>
key={i} <StyledTypeSelect
label={`Environment ${i + 1} name`} label='Type of environment'
value={environmentNames[i]} labelId={`environmentType${i}`}
onChange={(event) => { value={
setEnvironmentNames((names) => { environments[i]?.type ||
const newValues = [...names]; ENVIRONMENT_TYPES[0]
newValues[i] = event.target.value; }
return newValues; options={ENVIRONMENT_TYPES.map(
}); (type) => ({
key: type,
label: type,
}),
)}
onChange={(type) => {
const newEnvironments = [
...environments,
];
newEnvironments[i].type = type;
setEnvironments(newEnvironments);
}} }}
error={Boolean(error)}
errorText={error}
/> />
<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> </StyledEnvironmentNameInputs>

View File

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

View File

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