Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-7035] - Add DC-specific pricing to Linode backups #9588

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add DC-specific pricing to Linode backups ([#9588](https://github.com/linode/manager/pull/9588))
2 changes: 1 addition & 1 deletion packages/manager/src/cachedData/regions.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/manager/src/features/Backups/AutoEnroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const AutoEnroll = (props: AutoEnrollProps) => {
>
Backups pricing page
</Link>
.
</Typography>
</StyledDiv>
}
Expand Down
51 changes: 44 additions & 7 deletions packages/manager/src/features/Backups/BackupDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { styled } from '@mui/material';
import Stack from '@mui/material/Stack';
import { useSnackbar } from 'notistack';
import * as React from 'react';
Expand All @@ -16,6 +17,7 @@ import { TableRow } from 'src/components/TableRow';
import { TableRowError } from 'src/components/TableRowError/TableRowError';
import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
import { Typography } from 'src/components/Typography';
import { useFlags } from 'src/hooks/useFlags';
import {
useAccountSettings,
useMutateAccountSettings,
Expand All @@ -42,6 +44,8 @@ export const BackupDrawer = (props: Props) => {
const { onClose, open } = props;
const { enqueueSnackbar } = useSnackbar();

const flags = useFlags();

const {
data: linodes,
error: linodesError,
Expand Down Expand Up @@ -84,6 +88,13 @@ export const BackupDrawer = (props: Props) => {

const linodeCount = linodesWithoutBackups.length;

const backupsConfirmationHelperText = (
<>
Confirm to add backups to{' '}
<strong>{pluralize('Linode', 'Linodes', linodeCount)}</strong>.
</>
);

const renderBackupsTable = () => {
if (linodesLoading || typesLoading || accountSettingsLoading) {
return <TableRowLoading columns={3} />;
Expand Down Expand Up @@ -137,7 +148,12 @@ all new Linodes will automatically be backed up.`
};

return (
<Drawer onClose={onClose} open={open} title="Enable All Backups">
<Drawer
onClose={onClose}
open={open}
title="Enable All Backups"
wide={flags.dcSpecificPricing}
>
<Stack spacing={2}>
<Typography variant="body1">
Three backup slots are executed and rotated automatically: a daily
Expand All @@ -146,9 +162,8 @@ all new Linodes will automatically be backed up.`
<Link to="https://www.linode.com/docs/platform/disk-images/linode-backup-service/">
guide on Backups
</Link>{' '}
for more information on features and limitations. Confirm to add
backups to{' '}
<strong>{pluralize('Linode', 'Linodes', linodeCount)}</strong>.
for more information on features and limitations.{' '}
{!flags.dcSpecificPricing && backupsConfirmationHelperText}
</Typography>
{failedEnableBackupsCount > 0 && (
<Box>
Expand All @@ -168,12 +183,22 @@ all new Linodes will automatically be backed up.`
toggle={() => setShouldEnableAutoEnroll((prev) => !prev)}
/>
)}
<Box>
<StyledPricingBox>
{flags.dcSpecificPricing && (
<StyledTypography variant="h2">
Total for {pluralize('Linode', 'Linodes', linodeCount)}:
</StyledTypography>
)}
&nbsp;
<DisplayPrice
price={getTotalBackupsPrice({
flags,
linodes: linodesWithoutBackups,
types: types ?? [],
})}
interval="mo"
price={getTotalBackupsPrice(linodesWithoutBackups, types ?? [])}
/>
</Box>
</StyledPricingBox>
<ActionsPanel
primaryButtonProps={{
label: 'Confirm',
Expand All @@ -191,6 +216,7 @@ all new Linodes will automatically be backed up.`
<TableRow>
<TableCell>Label</TableCell>
<TableCell>Plan</TableCell>
{flags.dcSpecificPricing && <TableCell>Region</TableCell>}
<TableCell>Price</TableCell>
</TableRow>
</TableHead>
Expand All @@ -200,3 +226,14 @@ all new Linodes will automatically be backed up.`
</Drawer>
);
};

const StyledPricingBox = styled(Box, { label: 'StyledPricingBox' })(({}) => ({
alignItems: 'center',
display: 'flex',
}));

const StyledTypography = styled(Typography, { label: 'StyledTypography' })(
({ theme }) => ({
color: theme.palette.text.primary,
})
);
48 changes: 47 additions & 1 deletion packages/manager/src/features/Backups/BackupLinodeRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers';
import { BackupLinodeRow } from './BackupLinodeRow';

describe('BackupLinodeRow', () => {
it('should render linode, plan lable, and backups price', async () => {
it('should render linode, plan label, and backups price', async () => {
server.use(
rest.get('*/linode/types/linode-type-test', (req, res, ctx) => {
return res(
Expand All @@ -34,4 +34,50 @@ describe('BackupLinodeRow', () => {
expect(await findByText('Linode Test Type')).toBeVisible();
expect(await findByText('$12.99/mo')).toBeVisible();
});

it('should render region and region-specific pricing if dcSpecificPricing feature flag is on', async () => {
server.use(
rest.get('*/linode/types/linode-type-test', (req, res, ctx) => {
return res(
ctx.json(
linodeTypeFactory.build({
addons: {
backups: {
price: {
hourly: 0.004,
monthly: 2.5,
},
region_prices: [
{
hourly: 0.0048,
id: 'id-cgk',
monthly: 3.57,
},
],
},
},
label: 'Linode Test Type',
})
)
);
})
);

const linode = linodeFactory.build({
label: 'my-dc-pricing-linode-to-back-up',
region: 'id-cgk',
type: 'linode-type-test',
});

const { findByText, getByText } = renderWithTheme(
wrapWithTableBody(<BackupLinodeRow linode={linode} />, {
flags: { dcSpecificPricing: true },
})
);

expect(getByText('my-dc-pricing-linode-to-back-up')).toBeVisible();
expect(await findByText('Linode Test Type')).toBeVisible();
expect(await findByText('Jakarta, ID')).toBeVisible();
expect(await findByText('$3.57/mo')).toBeVisible();
});
});
24 changes: 22 additions & 2 deletions packages/manager/src/features/Backups/BackupLinodeRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Linode } from '@linode/api-v4';
import { Linode, PriceObject } from '@linode/api-v4';
import * as React from 'react';

import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';
import { Typography } from 'src/components/Typography';
import { useFlags } from 'src/hooks/useFlags';
import { useRegionsQuery } from 'src/queries/regions';
import { useTypeQuery } from 'src/queries/types';
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';

interface Props {
error?: string;
Expand All @@ -14,6 +17,18 @@ interface Props {
export const BackupLinodeRow = (props: Props) => {
const { error, linode } = props;
const { data: type } = useTypeQuery(linode.type ?? '', Boolean(linode.type));
const { data: regions } = useRegionsQuery();
const flags = useFlags();

const backupsMonthlyPrice: PriceObject['monthly'] = getMonthlyBackupsPrice({
flags,
region: linode.region,
type,
});

const regionLabel =
regions?.find((r) => r.id === linode.region)?.label ?? linode.region;

return (
<TableRow key={`backup-linode-${linode.id}`}>
<TableCell parentColumn="Label">
Expand All @@ -33,8 +48,13 @@ export const BackupLinodeRow = (props: Props) => {
<TableCell parentColumn="Plan">
{type?.label ?? linode.type ?? 'Unknown'}
</TableCell>
{flags.dcSpecificPricing && (
<TableCell parentColumn="Region">{regionLabel ?? 'Unknown'}</TableCell>
)}
<TableCell parentColumn="Price">
{`$${type?.addons.backups.price.monthly?.toFixed(2) ?? 'Unknown'}/mo`}
{backupsMonthlyPrice !== 0
? `$${backupsMonthlyPrice?.toFixed(2)}/mo`
: '$Unknown/mo'}
</TableCell>
</TableRow>
);
Expand Down
42 changes: 41 additions & 1 deletion packages/manager/src/features/Backups/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,46 @@ describe('getTotalBackupsPrice', () => {
addons: { backups: { price: { monthly: 2.5 } } },
id: 'my-type',
});
expect(getTotalBackupsPrice(linodes, types)).toBe(7.5);
expect(
getTotalBackupsPrice({
flags: { dcSpecificPricing: false },
linodes,
types,
})
).toBe(7.5);
});

it('correctly calculates the total price with DC-specific pricing for Linode backups', () => {
const basePriceLinodes = linodeFactory.buildList(2, { type: 'my-type' });
const priceIncreaseLinode = linodeFactory.build({
region: 'id-cgk',
type: 'my-type',
});
const linodes = [...basePriceLinodes, priceIncreaseLinode];
const types = linodeTypeFactory.buildList(1, {
addons: {
backups: {
price: {
hourly: 0.004,
monthly: 2.5,
},
region_prices: [
{
hourly: 0.0048,
id: 'id-cgk',
monthly: 3.57,
},
],
},
},
id: 'my-type',
});
expect(
getTotalBackupsPrice({
flags: { dcSpecificPricing: true },
linodes,
types,
})
).toBe(8.57);
});
});
39 changes: 33 additions & 6 deletions packages/manager/src/features/Backups/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import { enableBackups } from '@linode/api-v4';
import { useMutation, useQueryClient } from 'react-query';

import { FlagSet } from 'src/featureFlags';
import { queryKey } from 'src/queries/linodes/linodes';
import { pluralize } from 'src/utilities/pluralize';
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';

import type { APIError, Linode, LinodeType } from '@linode/api-v4';
import type { APIError, Linode, LinodeType, PriceObject } from '@linode/api-v4';

export const getTotalBackupsPrice = (
linodes: Linode[],
types: LinodeType[]
) => {
export interface TotalBackupsPriceOptions {
/**
* Our feature flags so we can determined whether or not to add price increase.
* @example { dcSpecificPricing: true }
*/
flags: FlagSet;
/**
* List of linodes without backups enabled
*/
linodes: Linode[];
/**
* List of types for the linodes without backups
*/
types: LinodeType[];
}

export const getTotalBackupsPrice = ({
flags,
linodes,
types,
}: TotalBackupsPriceOptions) => {
return linodes.reduce((prevValue: number, linode: Linode) => {
const type = types.find((type) => type.id === linode.type);
return prevValue + (type?.addons.backups.price.monthly ?? 0);

const backupsMonthlyPrice: PriceObject['monthly'] =
getMonthlyBackupsPrice({
flags,
region: linode.region,
type,
}) || 0;

return prevValue + backupsMonthlyPrice;
}, 0);
};

Expand Down
Loading