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:
parent
4167d772e9
commit
f5a2a18ffc
@ -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) => (
|
||||||
|
@ -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 {
|
||||||
|
@ -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' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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';
|
||||||
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
@ -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