mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01: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 {
|
||||
title?: ReactNode;
|
||||
description: string;
|
||||
description: ReactNode;
|
||||
documentationLink?: string;
|
||||
documentationIcon?: ReactNode;
|
||||
documentationLinkLabel?: string;
|
||||
@ -210,7 +210,7 @@ const StyledDescriptionCard = styled('article')(({ theme }) => ({
|
||||
marginBlockEnd: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')(({ theme }) => ({
|
||||
const StyledDescription = styled('div')(() => ({
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
@ -370,7 +370,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
||||
};
|
||||
|
||||
interface IMobileGuidance {
|
||||
description: string;
|
||||
description: ReactNode;
|
||||
documentationLink?: string;
|
||||
documentationIcon?: ReactNode;
|
||||
documentationLinkLabel?: string;
|
||||
@ -410,7 +410,7 @@ const MobileGuidance = ({
|
||||
};
|
||||
|
||||
interface IGuidanceProps {
|
||||
description: string;
|
||||
description: ReactNode;
|
||||
documentationIcon?: ReactNode;
|
||||
documentationLink?: string;
|
||||
documentationLinkLabel?: string;
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Table,
|
||||
TablePlaceholder,
|
||||
} from 'component/common/Table';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { Alert, styled, TableBody } from '@mui/material';
|
||||
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 type { IEnvironment } from 'interfaces/environments';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import { PurchasableFeature } from './PurchasableFeature/PurchasableFeature';
|
||||
import { OrderEnvironmentsDialog } from './OrderEnvironmentsDialog/OrderEnvironmentsDialog';
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(4),
|
||||
@ -37,7 +40,12 @@ export const EnvironmentTable = () => {
|
||||
const { changeSortOrder } = useEnvironmentApi();
|
||||
const { setToastApiError } = useToast();
|
||||
const { environments, mutateEnvironments } = useEnvironments();
|
||||
const [purchaseDialogOpen, setPurchaseDialogOpen] = useState(false);
|
||||
const isFeatureEnabled = useUiFlag('EEA');
|
||||
const isPurchaseAdditionalEnvronmentsEnabled = useUiFlag(
|
||||
'purchaseAdditionalEnvironments',
|
||||
);
|
||||
const { isPro } = useUiConfig();
|
||||
|
||||
const moveListItem: MoveListItem = useCallback(
|
||||
async (dragIndex: number, dropIndex: number, save = false) => {
|
||||
@ -58,6 +66,28 @@ export const EnvironmentTable = () => {
|
||||
[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 {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
@ -68,7 +98,7 @@ export const EnvironmentTable = () => {
|
||||
setGlobalFilter,
|
||||
} = useTable(
|
||||
{
|
||||
columns: COLUMNS as any,
|
||||
columns: columnsWithActions as any,
|
||||
data: environments,
|
||||
disableSortBy: true,
|
||||
},
|
||||
@ -91,7 +121,7 @@ export const EnvironmentTable = () => {
|
||||
<PageHeader title={`Environments (${count})`} actions={headerActions} />
|
||||
);
|
||||
|
||||
if (!isFeatureEnabled) {
|
||||
if (!isFeatureEnabled && !isPurchaseAdditionalEnvronmentsEnabled) {
|
||||
return (
|
||||
<PageContent header={header}>
|
||||
<PremiumFeature feature='environments' />
|
||||
@ -101,6 +131,20 @@ export const EnvironmentTable = () => {
|
||||
|
||||
return (
|
||||
<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'>
|
||||
This is the order of environments that you have today in each
|
||||
feature flag. Rearranging them here will change also the order
|
||||
@ -185,14 +229,4 @@ const COLUMNS = [
|
||||
row.apiTokenCount === 1 ? '1 token' : `${row.apiTokenCount} tokens`,
|
||||
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,
|
||||
"mobile": true,
|
||||
},
|
||||
"notFlag": "purchaseAdditionalEnvironments",
|
||||
"path": "/environments",
|
||||
"title": "Environments",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "purchaseAdditionalEnvironments",
|
||||
"menu": {
|
||||
"advanced": true,
|
||||
"mobile": true,
|
||||
},
|
||||
"path": "/environments",
|
||||
"title": "Environments",
|
||||
"type": "protected",
|
||||
|
@ -267,6 +267,15 @@ export const routes: IRoute[] = [
|
||||
component: EnvironmentTable,
|
||||
type: 'protected',
|
||||
flag: EEA,
|
||||
notFlag: 'purchaseAdditionalEnvironments',
|
||||
menu: { mobile: true, advanced: true },
|
||||
},
|
||||
{
|
||||
path: '/environments',
|
||||
title: 'Environments',
|
||||
component: EnvironmentTable,
|
||||
type: 'protected',
|
||||
flag: 'purchaseAdditionalEnvironments',
|
||||
menu: { mobile: true, advanced: true },
|
||||
},
|
||||
{
|
||||
|
@ -89,6 +89,7 @@ export type UiFlags = {
|
||||
onboardingUI?: boolean;
|
||||
eventTimeline?: boolean;
|
||||
personalDashboardUI?: boolean;
|
||||
purchaseAdditionalEnvironments?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
Loading…
Reference in New Issue
Block a user