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:
parent
4167d772e9
commit
f5a2a18ffc
@ -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) => (
|
||||
|
@ -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 {
|
||||
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user