mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
Frontend for additional environments (#8378)
This commit is contained in:
parent
99021f373f
commit
48eee2043f
@ -26,7 +26,7 @@ import { relative } from 'themes/themeStyles';
|
|||||||
|
|
||||||
interface ICreateProps {
|
interface ICreateProps {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
description: string;
|
description: ReactNode;
|
||||||
documentationLink?: string;
|
documentationLink?: string;
|
||||||
documentationIcon?: ReactNode;
|
documentationIcon?: ReactNode;
|
||||||
documentationLinkLabel?: string;
|
documentationLinkLabel?: string;
|
||||||
@ -210,7 +210,7 @@ const StyledDescriptionCard = styled('article')(({ theme }) => ({
|
|||||||
marginBlockEnd: theme.spacing(3),
|
marginBlockEnd: theme.spacing(3),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledDescription = styled('p')(({ theme }) => ({
|
const StyledDescription = styled('div')(() => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -370,7 +370,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IMobileGuidance {
|
interface IMobileGuidance {
|
||||||
description: string;
|
description: ReactNode;
|
||||||
documentationLink?: string;
|
documentationLink?: string;
|
||||||
documentationIcon?: ReactNode;
|
documentationIcon?: ReactNode;
|
||||||
documentationLinkLabel?: string;
|
documentationLinkLabel?: string;
|
||||||
@ -410,7 +410,7 @@ const MobileGuidance = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IGuidanceProps {
|
interface IGuidanceProps {
|
||||||
description: string;
|
description: ReactNode;
|
||||||
documentationIcon?: ReactNode;
|
documentationIcon?: ReactNode;
|
||||||
documentationLink?: string;
|
documentationLink?: string;
|
||||||
documentationLinkLabel?: string;
|
documentationLinkLabel?: string;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
TablePlaceholder,
|
TablePlaceholder,
|
||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { Alert, styled, TableBody } from '@mui/material';
|
import { Alert, styled, TableBody } from '@mui/material';
|
||||||
import type { MoveListItem } from 'hooks/useDragItem';
|
import type { MoveListItem } from 'hooks/useDragItem';
|
||||||
@ -27,7 +27,10 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli
|
|||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import type { IEnvironment } from 'interfaces/environments';
|
import type { IEnvironment } from 'interfaces/environments';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
|
import { PurchasableFeature } from './PurchasableFeature/PurchasableFeature';
|
||||||
|
import { OrderEnvironmentsDialog } from './OrderEnvironmentsDialog/OrderEnvironmentsDialog';
|
||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(4),
|
marginBottom: theme.spacing(4),
|
||||||
@ -37,7 +40,12 @@ export const EnvironmentTable = () => {
|
|||||||
const { changeSortOrder } = useEnvironmentApi();
|
const { changeSortOrder } = useEnvironmentApi();
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { environments, mutateEnvironments } = useEnvironments();
|
const { environments, mutateEnvironments } = useEnvironments();
|
||||||
|
const [purchaseDialogOpen, setPurchaseDialogOpen] = useState(false);
|
||||||
const isFeatureEnabled = useUiFlag('EEA');
|
const isFeatureEnabled = useUiFlag('EEA');
|
||||||
|
const isPurchaseAdditionalEnvronmentsEnabled = useUiFlag(
|
||||||
|
'purchaseAdditionalEnvironments',
|
||||||
|
);
|
||||||
|
const { isPro } = useUiConfig();
|
||||||
|
|
||||||
const moveListItem: MoveListItem = useCallback(
|
const moveListItem: MoveListItem = useCallback(
|
||||||
async (dragIndex: number, dropIndex: number, save = false) => {
|
async (dragIndex: number, dropIndex: number, save = false) => {
|
||||||
@ -58,6 +66,28 @@ export const EnvironmentTable = () => {
|
|||||||
[changeSortOrder, environments, mutateEnvironments, setToastApiError],
|
[changeSortOrder, environments, mutateEnvironments, setToastApiError],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const columnsWithActions = useMemo(() => {
|
||||||
|
if (isFeatureEnabled) {
|
||||||
|
return [
|
||||||
|
...COLUMNS,
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
width: '1%',
|
||||||
|
Cell: ({
|
||||||
|
row: { original },
|
||||||
|
}: { row: { original: IEnvironment } }) => (
|
||||||
|
<EnvironmentActionCell environment={original} />
|
||||||
|
),
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return COLUMNS;
|
||||||
|
}, [isFeatureEnabled]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
@ -68,7 +98,7 @@ export const EnvironmentTable = () => {
|
|||||||
setGlobalFilter,
|
setGlobalFilter,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns: COLUMNS as any,
|
columns: columnsWithActions as any,
|
||||||
data: environments,
|
data: environments,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
@ -91,7 +121,7 @@ export const EnvironmentTable = () => {
|
|||||||
<PageHeader title={`Environments (${count})`} actions={headerActions} />
|
<PageHeader title={`Environments (${count})`} actions={headerActions} />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isFeatureEnabled) {
|
if (!isFeatureEnabled && !isPurchaseAdditionalEnvronmentsEnabled) {
|
||||||
return (
|
return (
|
||||||
<PageContent header={header}>
|
<PageContent header={header}>
|
||||||
<PremiumFeature feature='environments' />
|
<PremiumFeature feature='environments' />
|
||||||
@ -101,6 +131,20 @@ export const EnvironmentTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent header={header}>
|
<PageContent header={header}>
|
||||||
|
{isPro() && isPurchaseAdditionalEnvronmentsEnabled ? (
|
||||||
|
<>
|
||||||
|
<PurchasableFeature
|
||||||
|
title='Purchase 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={() => {}} // TODO: API call
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<StyledAlert severity='info'>
|
<StyledAlert severity='info'>
|
||||||
This is the order of environments that you have today in each
|
This is the order of environments that you have today in each
|
||||||
feature flag. Rearranging them here will change also the order
|
feature flag. Rearranging them here will change also the order
|
||||||
@ -185,14 +229,4 @@ const COLUMNS = [
|
|||||||
row.apiTokenCount === 1 ? '1 token' : `${row.apiTokenCount} tokens`,
|
row.apiTokenCount === 1 ? '1 token' : `${row.apiTokenCount} tokens`,
|
||||||
Cell: TextCell,
|
Cell: TextCell,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Header: 'Actions',
|
|
||||||
id: 'Actions',
|
|
||||||
align: 'center',
|
|
||||||
width: '1%',
|
|
||||||
Cell: ({ row: { original } }: { row: { original: IEnvironment } }) => (
|
|
||||||
<EnvironmentActionCell environment={original} />
|
|
||||||
),
|
|
||||||
disableGlobalFilter: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
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');
|
||||||
|
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');
|
||||||
|
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(['Dev', 'Staging']);
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
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(['Dev', 'Staging']);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,179 @@
|
|||||||
|
import { useState, type FC } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
} 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';
|
||||||
|
|
||||||
|
type OrderEnvironmentsDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (environments: string[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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];
|
||||||
|
|
||||||
|
export const OrderEnvironmentsDialog: FC<OrderEnvironmentsDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const [selectedOption, setSelectedOption] = useState(OPTIONS[0]);
|
||||||
|
const [costCheckboxChecked, setCostCheckboxChecked] = useState(false);
|
||||||
|
const [environmentNames, setEnvironmentNames] = useState<string[]>([]);
|
||||||
|
|
||||||
|
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(environmentNames)}
|
||||||
|
>
|
||||||
|
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'
|
||||||
|
htmlFor='numberOfEnvironments'
|
||||||
|
>
|
||||||
|
Select the number of additional environments
|
||||||
|
</Typography>
|
||||||
|
<StyledGeneralSelect
|
||||||
|
id='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);
|
||||||
|
setEnvironmentNames((names) =>
|
||||||
|
names.slice(0, value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<StyledEnvironmentNameInputs>
|
||||||
|
<Typography>
|
||||||
|
How would you like the environment
|
||||||
|
{selectedOption > 1 ? 's' : ''} to be named?
|
||||||
|
</Typography>
|
||||||
|
{[...Array(selectedOption)].map((_, i) => (
|
||||||
|
<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;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Box, Card, styled, Typography } from '@mui/material';
|
||||||
|
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||||
|
|
||||||
|
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),
|
||||||
|
}));
|
||||||
|
|
||||||
|
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>
|
||||||
|
))}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
@ -0,0 +1,71 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledMessage>
|
||||||
|
<StyledIconContainer>
|
||||||
|
<Icon />
|
||||||
|
</StyledIconContainer>
|
||||||
|
<Box>
|
||||||
|
<Typography variant='h3'>{title}</Typography>
|
||||||
|
<Typography>{description}</Typography>
|
||||||
|
</Box>
|
||||||
|
</StyledMessage>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<Button variant='contained' onClick={onClick}>
|
||||||
|
View pricing
|
||||||
|
</Button>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -252,6 +252,18 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"advanced": true,
|
"advanced": true,
|
||||||
"mobile": true,
|
"mobile": true,
|
||||||
},
|
},
|
||||||
|
"notFlag": "purchaseAdditionalEnvironments",
|
||||||
|
"path": "/environments",
|
||||||
|
"title": "Environments",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "purchaseAdditionalEnvironments",
|
||||||
|
"menu": {
|
||||||
|
"advanced": true,
|
||||||
|
"mobile": true,
|
||||||
|
},
|
||||||
"path": "/environments",
|
"path": "/environments",
|
||||||
"title": "Environments",
|
"title": "Environments",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
|
@ -267,6 +267,15 @@ export const routes: IRoute[] = [
|
|||||||
component: EnvironmentTable,
|
component: EnvironmentTable,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
flag: EEA,
|
flag: EEA,
|
||||||
|
notFlag: 'purchaseAdditionalEnvironments',
|
||||||
|
menu: { mobile: true, advanced: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/environments',
|
||||||
|
title: 'Environments',
|
||||||
|
component: EnvironmentTable,
|
||||||
|
type: 'protected',
|
||||||
|
flag: 'purchaseAdditionalEnvironments',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -89,6 +89,7 @@ export type UiFlags = {
|
|||||||
onboardingUI?: boolean;
|
onboardingUI?: boolean;
|
||||||
eventTimeline?: boolean;
|
eventTimeline?: boolean;
|
||||||
personalDashboardUI?: boolean;
|
personalDashboardUI?: boolean;
|
||||||
|
purchaseAdditionalEnvironments?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
Loading…
Reference in New Issue
Block a user