mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into archive_table
This commit is contained in:
		
						commit
						aaaefa864d
					
				@ -9,7 +9,7 @@ import {
 | 
			
		||||
    InstanceState,
 | 
			
		||||
    InstancePlan,
 | 
			
		||||
} from 'interfaces/instance';
 | 
			
		||||
import { calculateTrialDaysRemaining } from 'utils/billing';
 | 
			
		||||
import { hasTrialExpired } from 'utils/instanceTrial';
 | 
			
		||||
import { GridRow } from 'component/common/GridRow/GridRow';
 | 
			
		||||
import { GridCol } from 'component/common/GridCol/GridCol';
 | 
			
		||||
import { GridColLink } from './GridColLink/GridColLink';
 | 
			
		||||
@ -81,7 +81,7 @@ interface IBillingPlanProps {
 | 
			
		||||
 | 
			
		||||
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
 | 
			
		||||
    const { users } = useUsers();
 | 
			
		||||
    const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus);
 | 
			
		||||
    const trialHasExpired = hasTrialExpired(instanceStatus);
 | 
			
		||||
 | 
			
		||||
    const price = {
 | 
			
		||||
        [InstancePlan.PRO]: 80,
 | 
			
		||||
@ -91,11 +91,6 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
 | 
			
		||||
        user: 15,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const statusExpired =
 | 
			
		||||
        instanceStatus.state === InstanceState.TRIAL &&
 | 
			
		||||
        typeof trialDaysRemaining === 'number' &&
 | 
			
		||||
        trialDaysRemaining <= 0;
 | 
			
		||||
 | 
			
		||||
    const planPrice = price[instanceStatus.plan];
 | 
			
		||||
    const seats = instanceStatus.seats ?? 5;
 | 
			
		||||
    const freeAssigned = Math.min(users.length, seats);
 | 
			
		||||
@ -135,12 +130,12 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
 | 
			
		||||
                                show={
 | 
			
		||||
                                    <StyledTrialSpan
 | 
			
		||||
                                        sx={theme => ({
 | 
			
		||||
                                            color: statusExpired
 | 
			
		||||
                                            color: trialHasExpired
 | 
			
		||||
                                                ? theme.palette.error.dark
 | 
			
		||||
                                                : theme.palette.warning.dark,
 | 
			
		||||
                                        })}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {statusExpired
 | 
			
		||||
                                        {trialHasExpired
 | 
			
		||||
                                            ? 'Trial expired'
 | 
			
		||||
                                            : instanceStatus.trialExtended
 | 
			
		||||
                                            ? 'Extended Trial'
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { IInstanceStatus, InstanceState } from 'interfaces/instance';
 | 
			
		||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import AccessContext from 'contexts/AccessContext';
 | 
			
		||||
import useInstanceStatusApi from 'hooks/api/actions/useInstanceStatusApi/useInstanceStatusApi';
 | 
			
		||||
import { calculateTrialDaysRemaining } from 'utils/billing';
 | 
			
		||||
import { hasTrialExpired } from 'utils/instanceTrial';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
 | 
			
		||||
@ -24,22 +24,16 @@ const TrialDialog: VFC<ITrialDialogProps> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus);
 | 
			
		||||
 | 
			
		||||
    const statusExpired =
 | 
			
		||||
        instanceStatus.state === InstanceState.TRIAL &&
 | 
			
		||||
        typeof trialDaysRemaining === 'number' &&
 | 
			
		||||
        trialDaysRemaining <= 0;
 | 
			
		||||
 | 
			
		||||
    const [dialogOpen, setDialogOpen] = useState(statusExpired);
 | 
			
		||||
    const trialHasExpired = hasTrialExpired(instanceStatus);
 | 
			
		||||
    const [dialogOpen, setDialogOpen] = useState(trialHasExpired);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setDialogOpen(statusExpired);
 | 
			
		||||
        setDialogOpen(trialHasExpired);
 | 
			
		||||
        const interval = setInterval(() => {
 | 
			
		||||
            setDialogOpen(statusExpired);
 | 
			
		||||
            setDialogOpen(trialHasExpired);
 | 
			
		||||
        }, 60000);
 | 
			
		||||
        return () => clearInterval(interval);
 | 
			
		||||
    }, [statusExpired]);
 | 
			
		||||
    }, [trialHasExpired]);
 | 
			
		||||
 | 
			
		||||
    if (hasAccess(ADMIN)) {
 | 
			
		||||
        return (
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { styled, Button, Typography } from '@mui/material';
 | 
			
		||||
import { IInstanceStatus, InstanceState } from 'interfaces/instance';
 | 
			
		||||
import { IInstanceStatus } from 'interfaces/instance';
 | 
			
		||||
import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds';
 | 
			
		||||
import { InfoOutlined, WarningAmber } from '@mui/icons-material';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
@ -7,7 +7,10 @@ import { useContext } from 'react';
 | 
			
		||||
import AccessContext from 'contexts/AccessContext';
 | 
			
		||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { calculateTrialDaysRemaining } from 'utils/billing';
 | 
			
		||||
import {
 | 
			
		||||
    hasTrialExpired,
 | 
			
		||||
    formatTrialExpirationWarning,
 | 
			
		||||
} from 'utils/instanceTrial';
 | 
			
		||||
 | 
			
		||||
const StyledWarningBar = styled('aside')(({ theme }) => ({
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
@ -59,14 +62,10 @@ export const InstanceStatusBar = ({
 | 
			
		||||
    instanceStatus,
 | 
			
		||||
}: IInstanceStatusBarProps) => {
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const trialHasExpired = hasTrialExpired(instanceStatus);
 | 
			
		||||
    const trialExpirationWarning = formatTrialExpirationWarning(instanceStatus);
 | 
			
		||||
 | 
			
		||||
    const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus);
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        instanceStatus.state === InstanceState.TRIAL &&
 | 
			
		||||
        typeof trialDaysRemaining === 'number' &&
 | 
			
		||||
        trialDaysRemaining <= 0
 | 
			
		||||
    ) {
 | 
			
		||||
    if (trialHasExpired) {
 | 
			
		||||
        return (
 | 
			
		||||
            <StyledWarningBar data-testid={INSTANCE_STATUS_BAR_ID}>
 | 
			
		||||
                <StyledWarningIcon />
 | 
			
		||||
@ -87,11 +86,7 @@ export const InstanceStatusBar = ({
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        instanceStatus.state === InstanceState.TRIAL &&
 | 
			
		||||
        typeof trialDaysRemaining === 'number' &&
 | 
			
		||||
        trialDaysRemaining <= 10
 | 
			
		||||
    ) {
 | 
			
		||||
    if (trialExpirationWarning) {
 | 
			
		||||
        return (
 | 
			
		||||
            <StyledInfoBar data-testid={INSTANCE_STATUS_BAR_ID}>
 | 
			
		||||
                <StyledInfoIcon />
 | 
			
		||||
@ -101,7 +96,7 @@ export const InstanceStatusBar = ({
 | 
			
		||||
                    })}
 | 
			
		||||
                >
 | 
			
		||||
                    <strong>Heads up!</strong> You have{' '}
 | 
			
		||||
                    <strong>{trialDaysRemaining} days</strong> left of your free{' '}
 | 
			
		||||
                    <strong>{trialExpirationWarning}</strong> left of your free{' '}
 | 
			
		||||
                    {instanceStatus.plan} trial.
 | 
			
		||||
                </Typography>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
 | 
			
		||||
@ -65,8 +65,7 @@ exports[`InstanceStatusBar should warn when the trial is about to expire 1`] = `
 | 
			
		||||
     You have
 | 
			
		||||
     
 | 
			
		||||
    <strong>
 | 
			
		||||
      4
 | 
			
		||||
       days
 | 
			
		||||
      4 days
 | 
			
		||||
    </strong>
 | 
			
		||||
     left of your free
 | 
			
		||||
     
 | 
			
		||||
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
import { differenceInDays, parseISO } from 'date-fns';
 | 
			
		||||
import { IInstanceStatus } from 'interfaces/instance';
 | 
			
		||||
 | 
			
		||||
export const calculateTrialDaysRemaining = (
 | 
			
		||||
    instanceStatus?: IInstanceStatus
 | 
			
		||||
): number | undefined => {
 | 
			
		||||
    return instanceStatus?.trialExpiry
 | 
			
		||||
        ? differenceInDays(parseISO(instanceStatus.trialExpiry), new Date())
 | 
			
		||||
        : undefined;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										66
									
								
								frontend/src/utils/instanceTrial.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								frontend/src/utils/instanceTrial.test.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
import {
 | 
			
		||||
    hasTrialExpired,
 | 
			
		||||
    formatTrialExpirationWarning,
 | 
			
		||||
} from 'utils/instanceTrial';
 | 
			
		||||
import { InstancePlan, InstanceState } from 'interfaces/instance';
 | 
			
		||||
import { subHours, addHours, addMinutes, subMinutes } from 'date-fns';
 | 
			
		||||
 | 
			
		||||
test.each([
 | 
			
		||||
    undefined,
 | 
			
		||||
    { plan: InstancePlan.UNKNOWN },
 | 
			
		||||
    { plan: InstancePlan.UNKNOWN, state: InstanceState.ACTIVE },
 | 
			
		||||
    { plan: InstancePlan.UNKNOWN, state: InstanceState.TRIAL },
 | 
			
		||||
    { plan: InstancePlan.COMPANY, state: InstanceState.TRIAL },
 | 
			
		||||
    { plan: InstancePlan.PRO, state: InstanceState.TRIAL },
 | 
			
		||||
])('unknown trial states should not count as expired', input => {
 | 
			
		||||
    expect(hasTrialExpired(input)).toEqual(false);
 | 
			
		||||
    expect(formatTrialExpirationWarning(input)).toEqual(undefined);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('hasTrialExpired', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
        hasTrialExpired({
 | 
			
		||||
            plan: InstancePlan.UNKNOWN,
 | 
			
		||||
            state: InstanceState.TRIAL,
 | 
			
		||||
            trialExpiry: subHours(new Date(), 2).toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
    ).toEqual(true);
 | 
			
		||||
    expect(
 | 
			
		||||
        hasTrialExpired({
 | 
			
		||||
            plan: InstancePlan.UNKNOWN,
 | 
			
		||||
            state: InstanceState.TRIAL,
 | 
			
		||||
            trialExpiry: addHours(new Date(), 2).toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
    ).toEqual(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('formatTrialExpirationWarning', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
        formatTrialExpirationWarning({
 | 
			
		||||
            plan: InstancePlan.UNKNOWN,
 | 
			
		||||
            state: InstanceState.TRIAL,
 | 
			
		||||
            trialExpiry: subMinutes(new Date(), 1).toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
    ).toEqual(undefined);
 | 
			
		||||
    expect(
 | 
			
		||||
        formatTrialExpirationWarning({
 | 
			
		||||
            plan: InstancePlan.UNKNOWN,
 | 
			
		||||
            state: InstanceState.TRIAL,
 | 
			
		||||
            trialExpiry: addMinutes(new Date(), 23 * 60 + 1).toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
    ).toEqual('23 hours');
 | 
			
		||||
    expect(
 | 
			
		||||
        formatTrialExpirationWarning({
 | 
			
		||||
            plan: InstancePlan.UNKNOWN,
 | 
			
		||||
            state: InstanceState.TRIAL,
 | 
			
		||||
            trialExpiry: addHours(new Date(), 25).toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
    ).toEqual('1 day');
 | 
			
		||||
    expect(
 | 
			
		||||
        formatTrialExpirationWarning({
 | 
			
		||||
            plan: InstancePlan.UNKNOWN,
 | 
			
		||||
            state: InstanceState.TRIAL,
 | 
			
		||||
            trialExpiry: addHours(new Date(), 24 * 11 - 1).toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
    ).toEqual('10 days');
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										48
									
								
								frontend/src/utils/instanceTrial.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/src/utils/instanceTrial.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
import { parseISO, formatDistanceToNowStrict, isPast } from 'date-fns';
 | 
			
		||||
import { IInstanceStatus, InstanceState } from 'interfaces/instance';
 | 
			
		||||
import differenceInDays from 'date-fns/differenceInDays';
 | 
			
		||||
 | 
			
		||||
const TRIAL_EXPIRATION_WARNING_DAYS_THRESHOLD = 10;
 | 
			
		||||
 | 
			
		||||
export const hasTrialExpired = (
 | 
			
		||||
    instanceStatus: IInstanceStatus | undefined
 | 
			
		||||
): boolean => {
 | 
			
		||||
    const trialExpiry = parseTrialExpiryDate(instanceStatus);
 | 
			
		||||
 | 
			
		||||
    if (!trialExpiry) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return isPast(trialExpiry);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formatTrialExpirationWarning = (
 | 
			
		||||
    instanceStatus: IInstanceStatus | undefined
 | 
			
		||||
): string | undefined => {
 | 
			
		||||
    const trialExpiry = parseTrialExpiryDate(instanceStatus);
 | 
			
		||||
 | 
			
		||||
    if (!trialExpiry || isPast(trialExpiry)) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        differenceInDays(trialExpiry, new Date()) <=
 | 
			
		||||
        TRIAL_EXPIRATION_WARNING_DAYS_THRESHOLD
 | 
			
		||||
    ) {
 | 
			
		||||
        return formatDistanceToNowStrict(trialExpiry, {
 | 
			
		||||
            roundingMethod: 'floor',
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const parseTrialExpiryDate = (
 | 
			
		||||
    instanceStatus: IInstanceStatus | undefined
 | 
			
		||||
): Date | undefined => {
 | 
			
		||||
    if (
 | 
			
		||||
        instanceStatus &&
 | 
			
		||||
        instanceStatus.state === InstanceState.TRIAL &&
 | 
			
		||||
        instanceStatus.trialExpiry
 | 
			
		||||
    ) {
 | 
			
		||||
        return parseISO(instanceStatus.trialExpiry);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user