mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
[Gitar] Cleaning up stale flag: purchaseAdditionalEnvironments with value false (#8955)
[![Gitar](https://raw.githubusercontent.com/gitarcode/.github/main/assets/gitar-banner.svg)](https://gitar.ai) This automated PR permanently removes the `purchaseAdditionalEnvironments` feature flag. --- This automated PR was generated by [Gitar](https://gitar.ai). View [docs](https://gitar.ai/docs). --------- Co-authored-by: Gitar <noreply@gitar.ai> Co-authored-by: sjaanus <sellinjaanus@gmail.com>
This commit is contained in:
parent
eb7e2a655d
commit
8c189cabd2
@ -28,7 +28,6 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import type { IEnvironment } from 'interfaces/environments';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import { OrderEnvironments } from './OrderEnvironments/OrderEnvironments';
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(4),
|
||||
}));
|
||||
@ -38,9 +37,6 @@ export const EnvironmentTable = () => {
|
||||
const { setToastApiError } = useToast();
|
||||
const { environments, mutateEnvironments } = useEnvironments();
|
||||
const isFeatureEnabled = useUiFlag('EEA');
|
||||
const isPurchaseAdditionalEnvironmentsEnabled = useUiFlag(
|
||||
'purchaseAdditionalEnvironments',
|
||||
);
|
||||
|
||||
const moveListItem: MoveListItem = useCallback(
|
||||
async (dragIndex: number, dropIndex: number, save = false) => {
|
||||
@ -116,7 +112,7 @@ export const EnvironmentTable = () => {
|
||||
<PageHeader title={`Environments (${count})`} actions={headerActions} />
|
||||
);
|
||||
|
||||
if (!isFeatureEnabled && !isPurchaseAdditionalEnvironmentsEnabled) {
|
||||
if (!isFeatureEnabled) {
|
||||
return (
|
||||
<PageContent header={header}>
|
||||
<PremiumFeature feature='environments' />
|
||||
@ -126,7 +122,6 @@ export const EnvironmentTable = () => {
|
||||
|
||||
return (
|
||||
<PageContent header={header}>
|
||||
<OrderEnvironments />
|
||||
<StyledAlert severity='info'>
|
||||
This is the order of environments that you have today in each
|
||||
feature flag. Rearranging them here will change also the order
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { OrderEnvironments } from './OrderEnvironments';
|
||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||
|
||||
const server = testServerSetup();
|
||||
|
||||
const setupServerRoutes = (changeRequestsEnabled = true) => {
|
||||
testServerRoute(server, '/api/admin/ui-config', {
|
||||
environment: 'Pro',
|
||||
flags: {
|
||||
purchaseAdditionalEnvironments: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('OrderEnvironmentsDialog Component', () => {
|
||||
test('should show error if environment name is empty', async () => {
|
||||
setupServerRoutes();
|
||||
render(<OrderEnvironments />);
|
||||
|
||||
await waitFor(async () => {
|
||||
const openDialogButton = await screen.queryByRole('button', {
|
||||
name: /view pricing/i,
|
||||
});
|
||||
expect(openDialogButton).toBeInTheDocument();
|
||||
fireEvent.click(openDialogButton!);
|
||||
});
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', {
|
||||
name: /i understand adding environments/i,
|
||||
});
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /order/i });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(
|
||||
screen.getByText(/environment name is required/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
import { useState, type FC } from 'react';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { PurchasableFeature } from './PurchasableFeature/PurchasableFeature';
|
||||
import { OrderEnvironmentsDialog } from './OrderEnvironmentsDialog/OrderEnvironmentsDialog';
|
||||
import { OrderEnvironmentsConfirmation } from './OrderEnvironmentsConfirmation/OrderEnvironmentsConfirmation';
|
||||
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 = {};
|
||||
|
||||
export const OrderEnvironments: FC<OrderEnvironmentsProps> = () => {
|
||||
const [purchaseDialogOpen, setPurchaseDialogOpen] = useState(false);
|
||||
const [confirmationState, setConfirmationState] = useState<{
|
||||
isOpen: boolean;
|
||||
environmentsCount?: number;
|
||||
}>({ isOpen: false });
|
||||
const { isPro } = useUiConfig();
|
||||
const isPurchaseAdditionalEnvironmentsEnabled = useUiFlag(
|
||||
'purchaseAdditionalEnvironments',
|
||||
);
|
||||
const errors = useFormErrors();
|
||||
const { orderEnvironments } = useOrderEnvironmentApi();
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
if (!isPro() || !isPurchaseAdditionalEnvironmentsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit = async (
|
||||
environments: OrderEnvironmentsSchema['environments'],
|
||||
) => {
|
||||
let hasErrors = false;
|
||||
environments.forEach((environment, index) => {
|
||||
const field = `environment-${index}`;
|
||||
const environmentName = environment.name.trim();
|
||||
if (environmentName === '') {
|
||||
errors.setFormError(field, 'Environment name is required');
|
||||
hasErrors = true;
|
||||
} else {
|
||||
errors.removeFormError(field);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
await orderEnvironments({ environments });
|
||||
setPurchaseDialogOpen(false);
|
||||
setConfirmationState({
|
||||
isOpen: true,
|
||||
environmentsCount: environments.length,
|
||||
});
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PurchasableFeature
|
||||
title='Order additional environments'
|
||||
description='With our Pro plan, you now have the flexibility to expand your workspace by adding up to three additional environments.'
|
||||
onClick={() => setPurchaseDialogOpen(true)}
|
||||
/>
|
||||
<OrderEnvironmentsDialog
|
||||
open={purchaseDialogOpen}
|
||||
onClose={() => setPurchaseDialogOpen(false)}
|
||||
onSubmit={onSubmit}
|
||||
errors={errors}
|
||||
/>
|
||||
<OrderEnvironmentsConfirmation
|
||||
open={confirmationState.isOpen}
|
||||
orderedEnvironments={confirmationState.environmentsCount || 0}
|
||||
onClose={() => setConfirmationState({ isOpen: false })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
|
||||
type OrderEnvironmentsConfirmationProps = {
|
||||
open: boolean;
|
||||
orderedEnvironments: number;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const OrderEnvironmentsConfirmation: FC<
|
||||
OrderEnvironmentsConfirmationProps
|
||||
> = ({ open, orderedEnvironments, onClose }) => {
|
||||
return (
|
||||
<Dialogue
|
||||
open={open}
|
||||
title='Order confirmed'
|
||||
onClick={onClose}
|
||||
primaryButtonText='Close'
|
||||
>
|
||||
<Typography>
|
||||
You have ordered <strong>{orderedEnvironments}</strong>{' '}
|
||||
additional{' '}
|
||||
{orderedEnvironments === 1 ? 'environment' : 'environments'}. It
|
||||
may take up to 24 hours before you will get access.
|
||||
</Typography>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
@ -1,212 +0,0 @@
|
||||
import { vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { OrderEnvironmentsDialog } from './OrderEnvironmentsDialog';
|
||||
|
||||
describe('OrderEnvironmentsDialog Component', () => {
|
||||
const renderComponent = (props = {}) =>
|
||||
render(
|
||||
<OrderEnvironmentsDialog
|
||||
open={true}
|
||||
onClose={() => {}}
|
||||
onSubmit={() => {}}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
test('should disable "Order" button until the checkbox is checked', () => {
|
||||
renderComponent();
|
||||
|
||||
const orderButton = screen.getByRole('button', { name: /order/i });
|
||||
const checkbox = screen.getByRole('checkbox', {
|
||||
name: /i understand adding environments leads to extra costs/i,
|
||||
});
|
||||
|
||||
expect(orderButton).toBeDisabled();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(orderButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should render correct number of environment name inputs based on selected environments', () => {
|
||||
renderComponent();
|
||||
|
||||
let environmentInputs =
|
||||
screen.getAllByLabelText(/environment \d+ name/i);
|
||||
expect(environmentInputs).toHaveLength(1);
|
||||
|
||||
const selectButton = screen.getByRole('combobox', {
|
||||
name: /select the number of additional environments/i,
|
||||
});
|
||||
fireEvent.mouseDown(selectButton);
|
||||
|
||||
const option2 = screen.getByRole('option', { name: '2 environments' });
|
||||
fireEvent.click(option2);
|
||||
|
||||
environmentInputs = screen.getAllByLabelText(/environment \d+ name/i);
|
||||
expect(environmentInputs).toHaveLength(2);
|
||||
|
||||
fireEvent.mouseDown(selectButton);
|
||||
const option3 = screen.getByRole('option', { name: '3 environments' });
|
||||
fireEvent.click(option3);
|
||||
|
||||
environmentInputs = screen.getAllByLabelText(/environment \d+ name/i);
|
||||
expect(environmentInputs).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('should enable "Order" button only when checkbox is checked', () => {
|
||||
renderComponent();
|
||||
|
||||
const orderButton = screen.getByRole('button', { name: /order/i });
|
||||
const checkbox = screen.getByRole('checkbox', {
|
||||
name: /i understand adding environments leads to extra costs/i,
|
||||
});
|
||||
|
||||
expect(orderButton).toBeDisabled();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(orderButton).toBeEnabled();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(orderButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should output environment names', () => {
|
||||
const onSubmitMock = vi.fn();
|
||||
renderComponent({ onSubmit: onSubmitMock });
|
||||
|
||||
const selectButton = screen.getByRole('combobox', {
|
||||
name: /select the number of additional environments/i,
|
||||
});
|
||||
fireEvent.mouseDown(selectButton);
|
||||
|
||||
const option2 = screen.getByRole('option', { name: '2 environments' });
|
||||
fireEvent.click(option2);
|
||||
|
||||
const environmentInputs =
|
||||
screen.getAllByLabelText(/environment \d+ name/i);
|
||||
|
||||
fireEvent.change(environmentInputs[0], { target: { value: 'Dev' } });
|
||||
fireEvent.change(environmentInputs[1], {
|
||||
target: { value: 'Staging' },
|
||||
});
|
||||
const checkbox = screen.getByRole('checkbox', {
|
||||
name: /i understand adding environments leads to extra costs/i,
|
||||
});
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /order/i });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(onSubmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmitMock).toHaveBeenCalledWith([
|
||||
{ name: 'Dev', type: 'development' },
|
||||
{ name: 'Staging', type: 'development' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('should call onClose when "Cancel" button is clicked', () => {
|
||||
const onCloseMock = vi.fn();
|
||||
renderComponent({ onClose: onCloseMock });
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should adjust environment name inputs when decreasing environments', () => {
|
||||
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: '3 environments' });
|
||||
fireEvent.click(option3);
|
||||
|
||||
let environmentInputs =
|
||||
screen.getAllByLabelText(/environment \d+ name/i);
|
||||
expect(environmentInputs).toHaveLength(3);
|
||||
|
||||
fireEvent.change(environmentInputs[0], { target: { value: 'Dev' } });
|
||||
fireEvent.change(environmentInputs[1], {
|
||||
target: { value: 'Staging' },
|
||||
});
|
||||
fireEvent.change(environmentInputs[2], { target: { value: 'Prod' } });
|
||||
|
||||
fireEvent.mouseDown(selectButton);
|
||||
const option2 = screen.getByRole('option', { name: '2 environments' });
|
||||
fireEvent.click(option2);
|
||||
|
||||
environmentInputs = screen.getAllByLabelText(/environment \d+ name/i);
|
||||
expect(environmentInputs).toHaveLength(2);
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', {
|
||||
name: /i understand adding environments leads to extra costs/i,
|
||||
});
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /order/i });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(onSubmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmitMock).toHaveBeenCalledWith([
|
||||
{ name: 'Dev', type: 'development' },
|
||||
{ name: 'Staging', 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' },
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,253 +0,0 @@
|
||||
import { useState, type FC } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
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 type { IFormErrors } from 'hooks/useFormErrors';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import type { OrderEnvironmentsSchemaEnvironmentsItem } from 'openapi';
|
||||
|
||||
type OrderEnvironmentsDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (environments: OrderEnvironmentsSchemaEnvironmentsItem[]) => void;
|
||||
errors?: IFormErrors;
|
||||
};
|
||||
|
||||
const StyledDialog = styled(Dialog)(({ theme }) => ({
|
||||
maxWidth: '940px',
|
||||
margin: 'auto',
|
||||
'& .MuiDialog-paper': {
|
||||
borderRadius: theme.shape.borderRadiusExtraLarge,
|
||||
maxWidth: theme.spacing(170),
|
||||
width: '100%',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
padding: 0,
|
||||
'& .MuiPaper-root > section': {
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledTitle = styled('div')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledFooter = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
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',
|
||||
gap: theme.spacing(2),
|
||||
paddingTop: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledEnvironmentNameInputs = styled('fieldset')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
gap: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledCheckbox = styled(Checkbox)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(0.4),
|
||||
}));
|
||||
|
||||
const PRICE = 10;
|
||||
const OPTIONS = [1, 2, 3];
|
||||
const ENVIRONMENT_TYPES = [
|
||||
'development',
|
||||
'testing',
|
||||
'pre-production',
|
||||
'production',
|
||||
];
|
||||
|
||||
export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
errors,
|
||||
}) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const [selectedOption, setSelectedOption] = useState(OPTIONS[0]);
|
||||
const [costCheckboxChecked, setCostCheckboxChecked] = useState(false);
|
||||
const [environments, setEnvironments] = useState<
|
||||
{ name: string; type: string }[]
|
||||
>([{ name: '', type: ENVIRONMENT_TYPES[0] }]);
|
||||
|
||||
const trackEnvironmentSelect = () => {
|
||||
trackEvent('order-environments', {
|
||||
props: {
|
||||
eventType: 'selected environment count',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onTypeChange = (index: number, type: string) => {
|
||||
setEnvironments(
|
||||
environments.map((env, i) =>
|
||||
i === index ? { ...env, type } : { ...env },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const onNameChange = (index: number, name: string) => {
|
||||
setEnvironments(
|
||||
environments.map((env, i) =>
|
||||
i === index ? { ...env, name } : { ...env },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledDialog open={open} title=''>
|
||||
<FormTemplate
|
||||
compact
|
||||
description={
|
||||
<OrderEnvironmentsDialogPricing
|
||||
pricingOptions={OPTIONS.map((option) => ({
|
||||
environments: option,
|
||||
price: option * PRICE,
|
||||
}))}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<StyledFooter>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
variant='contained'
|
||||
disabled={!costCheckboxChecked}
|
||||
onClick={() => onSubmit(environments)}
|
||||
>
|
||||
Order
|
||||
</Button>
|
||||
</StyledFooter>
|
||||
}
|
||||
>
|
||||
<StyledTitle>
|
||||
<Typography variant='h3' component='div'>
|
||||
Order additional environments
|
||||
</Typography>
|
||||
</StyledTitle>
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
With our PRO plan, you have the flexibility to expand your
|
||||
workspace by adding environments at ${PRICE} per user per
|
||||
month.
|
||||
</Typography>
|
||||
<StyledFields>
|
||||
<Box>
|
||||
<Typography component='label' id='numberOfEnvironments'>
|
||||
Select the number of additional environments
|
||||
</Typography>
|
||||
<StyledGeneralSelect
|
||||
labelId='numberOfEnvironments'
|
||||
value={`${selectedOption}`}
|
||||
options={OPTIONS.map((option) => ({
|
||||
key: `${option}`,
|
||||
label: `${option} environment${option > 1 ? 's' : ''}`,
|
||||
}))}
|
||||
onChange={(option) => {
|
||||
const value = Number.parseInt(option, 10);
|
||||
setSelectedOption(value);
|
||||
setEnvironments((envs) =>
|
||||
[
|
||||
...envs,
|
||||
...Array(value).fill({
|
||||
name: '',
|
||||
type: ENVIRONMENT_TYPES[0],
|
||||
}),
|
||||
].slice(0, value),
|
||||
);
|
||||
trackEnvironmentSelect();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<StyledEnvironmentNameInputs>
|
||||
<Typography>
|
||||
How would you like the environment
|
||||
{selectedOption > 1 ? 's' : ''} to be named?
|
||||
</Typography>
|
||||
{[...Array(selectedOption)].map((_, i) => {
|
||||
const error = errors?.getFormError(
|
||||
`environment-${i}`,
|
||||
);
|
||||
return (
|
||||
<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) => {
|
||||
onTypeChange(i, type);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
size='small'
|
||||
label={`Environment ${i + 1} Name`}
|
||||
value={environments[i]?.name || ''}
|
||||
onChange={(e) => {
|
||||
onNameChange(i, e.target.value);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
</StyledEnvironmentInputs>
|
||||
);
|
||||
})}
|
||||
</StyledEnvironmentNameInputs>
|
||||
<Box>
|
||||
<StyledCheckbox
|
||||
edge='start'
|
||||
id='costsCheckbox'
|
||||
checked={costCheckboxChecked}
|
||||
onChange={() =>
|
||||
setCostCheckboxChecked((state) => !state)
|
||||
}
|
||||
/>
|
||||
<Typography component='label' htmlFor='costsCheckbox'>
|
||||
I understand adding environments leads to extra
|
||||
costs
|
||||
</Typography>
|
||||
</Box>
|
||||
</StyledFields>
|
||||
</FormTemplate>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
@ -1,71 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
import { Box, Card, styled, Typography } from '@mui/material';
|
||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||
import { BILLING_PRO_DEFAULT_INCLUDED_SEATS } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||
|
||||
type OrderEnvironmentsDialogPricingProps = {
|
||||
pricingOptions: Array<{ environments: number; price: number }>;
|
||||
};
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
marginTop: theme.spacing(7.5),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledCard = styled(Card)(({ theme }) => ({
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
boxShadow: 'none',
|
||||
}));
|
||||
|
||||
const StyledCardContent = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row',
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledExtraText = styled('div')(({ theme }) => ({
|
||||
paddingTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const OrderEnvironmentsDialogPricing: FC<
|
||||
OrderEnvironmentsDialogPricingProps
|
||||
> = ({ pricingOptions }) => (
|
||||
<StyledContainer>
|
||||
<Typography variant='h3' component='div' color='white' gutterBottom>
|
||||
Pricing
|
||||
</Typography>
|
||||
{pricingOptions.map((option) => (
|
||||
<StyledCard key={option.environments}>
|
||||
<StyledCardContent>
|
||||
<EnvironmentIcon enabled />
|
||||
<Box>
|
||||
<Box>
|
||||
<Typography variant='body2' fontWeight='bold'>
|
||||
{option.environments} additional environment
|
||||
{option.environments > 1 ? 's' : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant='body2'>
|
||||
${option.price} per user per month
|
||||
</Typography>
|
||||
</Box>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
))}
|
||||
<StyledExtraText>
|
||||
<Typography variant='body2' color='white'>
|
||||
With Pro, there is a minimum of{' '}
|
||||
{BILLING_PRO_DEFAULT_INCLUDED_SEATS} users, meaning an
|
||||
additional environment will cost at least $50 per month.
|
||||
</Typography>
|
||||
</StyledExtraText>
|
||||
</StyledContainer>
|
||||
);
|
@ -1,83 +0,0 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { Box, Button, styled, Typography } from '@mui/material';
|
||||
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
|
||||
import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg';
|
||||
import { ReactComponent as ProPlanIconLight } from 'assets/icons/pro-enterprise-feature-badge-light.svg';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
|
||||
type PurchasableFeatureProps = {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const Icon = () => (
|
||||
<ThemeMode darkmode={<ProPlanIconLight />} lightmode={<ProPlanIcon />} />
|
||||
);
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2, 3),
|
||||
marginBottom: theme.spacing(3),
|
||||
background: theme.palette.background.elevation2,
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(3),
|
||||
[theme.breakpoints.down('md')]: {
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledIconContainer = styled(Box)(() => ({
|
||||
width: '36px',
|
||||
flexShrink: 0,
|
||||
}));
|
||||
|
||||
const StyledMessage = styled(Box)(() => ({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled(Box)(() => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
}));
|
||||
|
||||
export const PurchasableFeature: FC<PurchasableFeatureProps> = ({
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
}) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
|
||||
const onViewPricingClick = () => {
|
||||
onClick();
|
||||
trackEvent('order-environments', {
|
||||
props: {
|
||||
eventType: 'view pricing clicked',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledMessage>
|
||||
<StyledIconContainer>
|
||||
<Icon />
|
||||
</StyledIconContainer>
|
||||
<Box>
|
||||
<Typography variant='h3'>{title}</Typography>
|
||||
<Typography>{description}</Typography>
|
||||
</Box>
|
||||
</StyledMessage>
|
||||
<StyledButtonContainer>
|
||||
<Button variant='contained' onClick={onViewPricingClick}>
|
||||
View pricing
|
||||
</Button>
|
||||
</StyledButtonContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
import type { OrderEnvironmentsSchema } from 'openapi';
|
||||
|
||||
export const useOrderEnvironmentApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const orderEnvironments = async (payload: OrderEnvironmentsSchema) => {
|
||||
const req = createRequest('api/admin/order-environments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
return {
|
||||
orderEnvironments,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
@ -85,7 +85,6 @@ export type UiFlags = {
|
||||
manyStrategiesPagination?: boolean;
|
||||
enableLegacyVariants?: boolean;
|
||||
flagCreator?: boolean;
|
||||
purchaseAdditionalEnvironments?: boolean;
|
||||
unleashAI?: boolean;
|
||||
releasePlans?: boolean;
|
||||
'enterprise-payg'?: boolean;
|
||||
|
@ -103,47 +103,6 @@ test('should strip special characters from email subject', async () => {
|
||||
expect(emailService.stripSpecialCharacters('tom-jones')).toBe('tom-jones');
|
||||
});
|
||||
|
||||
test('Can send order environments email', async () => {
|
||||
process.env.ORDER_ENVIRONMENTS_BCC = 'bcc@bcc.com';
|
||||
const emailService = new EmailService({
|
||||
email: {
|
||||
host: 'test',
|
||||
port: 587,
|
||||
secure: false,
|
||||
smtpuser: '',
|
||||
smtppass: '',
|
||||
sender: 'noreply@getunleash.ai',
|
||||
},
|
||||
getLogger: noLoggerProvider,
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
const customerId = 'customer133';
|
||||
const environments = [
|
||||
{ name: 'test', type: 'development' },
|
||||
{ name: 'live', type: 'production' },
|
||||
];
|
||||
|
||||
const content = await emailService.sendOrderEnvironmentEmail(
|
||||
'user@user.com',
|
||||
customerId,
|
||||
environments,
|
||||
);
|
||||
expect(content.from).toBe('noreply@getunleash.ai');
|
||||
expect(content.subject).toBe('Unleash - ordered environments successfully');
|
||||
expect(
|
||||
content.html.includes(
|
||||
`<li>Name: ${environments[0].name}, Type: ${environments[0].type}</li>`,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
content.html.includes(
|
||||
`<li>Name: ${environments[1].name}, Type: ${environments[1].type}</li>`,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(content.html.includes(customerId)).toBe(true);
|
||||
expect(content.bcc).toBe('bcc@bcc.com');
|
||||
});
|
||||
|
||||
test('Can send productivity report email', async () => {
|
||||
const emailService = new EmailService({
|
||||
server: {
|
||||
|
@ -70,11 +70,6 @@ export type ChangeRequestScheduleConflictData =
|
||||
environment: string;
|
||||
};
|
||||
|
||||
export type OrderEnvironmentData = {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export class EmailService {
|
||||
private logger: Logger;
|
||||
private config: IUnleashConfig;
|
||||
@ -462,71 +457,6 @@ export class EmailService {
|
||||
});
|
||||
}
|
||||
|
||||
async sendOrderEnvironmentEmail(
|
||||
userEmail: string,
|
||||
customerId: string,
|
||||
environments: OrderEnvironmentData[],
|
||||
): Promise<IEmailEnvelope> {
|
||||
if (this.configured()) {
|
||||
const context = {
|
||||
userEmail,
|
||||
customerId,
|
||||
environments: environments.map((data) => ({
|
||||
name: this.stripSpecialCharacters(data.name),
|
||||
type: this.stripSpecialCharacters(data.type),
|
||||
})),
|
||||
};
|
||||
|
||||
const bodyHtml = await this.compileTemplate(
|
||||
'order-environments',
|
||||
TemplateFormat.HTML,
|
||||
context,
|
||||
);
|
||||
const bodyText = await this.compileTemplate(
|
||||
'order-environments',
|
||||
TemplateFormat.PLAIN,
|
||||
context,
|
||||
);
|
||||
const email = {
|
||||
from: this.sender,
|
||||
to: userEmail,
|
||||
bcc:
|
||||
process.env.ORDER_ENVIRONMENTS_BCC ||
|
||||
'pro-sales@getunleash.io',
|
||||
subject: ORDER_ENVIRONMENTS_SUBJECT,
|
||||
html: bodyHtml,
|
||||
text: bodyText,
|
||||
};
|
||||
process.nextTick(() => {
|
||||
this.mailer!.sendMail(email).then(
|
||||
() =>
|
||||
this.logger.info(
|
||||
'Successfully sent order environments email',
|
||||
),
|
||||
(e) =>
|
||||
this.logger.warn(
|
||||
'Failed to send order environments email',
|
||||
e,
|
||||
),
|
||||
);
|
||||
});
|
||||
return Promise.resolve(email);
|
||||
}
|
||||
return new Promise((res) => {
|
||||
this.logger.warn(
|
||||
'No mailer is configured. Please read the docs on how to configure an email service',
|
||||
);
|
||||
res({
|
||||
from: this.sender,
|
||||
to: userEmail,
|
||||
bcc: '',
|
||||
subject: ORDER_ENVIRONMENTS_SUBJECT,
|
||||
html: '',
|
||||
text: '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async sendProductivityReportEmail(
|
||||
userEmail: string,
|
||||
userName: string,
|
||||
|
@ -47,7 +47,6 @@ export type IFlagKey =
|
||||
| 'extendedMetrics'
|
||||
| 'removeUnsafeInlineStyleSrc'
|
||||
| 'projectRoleAssignment'
|
||||
| 'purchaseAdditionalEnvironments'
|
||||
| 'originMiddlewareRequestLogging'
|
||||
| 'unleashAI'
|
||||
| 'webhookDomainLogging'
|
||||
@ -239,10 +238,6 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_PROJECT_ROLE_ASSIGNMENT,
|
||||
false,
|
||||
),
|
||||
purchaseAdditionalEnvironments: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_PURCHASE_ADDITIONAL_ENVIRONMENTS,
|
||||
false,
|
||||
),
|
||||
originMiddlewareRequestLogging: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_ORIGIN_MIDDLEWARE_REQUEST_LOGGING,
|
||||
false,
|
||||
|
@ -48,7 +48,6 @@ process.nextTick(async () => {
|
||||
manyStrategiesPagination: true,
|
||||
enableLegacyVariants: false,
|
||||
extendedMetrics: true,
|
||||
purchaseAdditionalEnvironments: true,
|
||||
originMiddlewareRequestLogging: true,
|
||||
unleashAI: true,
|
||||
webhookDomainLogging: true,
|
||||
|
Loading…
Reference in New Issue
Block a user