Skip to content

Commit 7ddcf3d

Browse files
authored
feat: [M3-7035] - Add DC-specific pricing to Linode backups (#9588)
* Add dynamic pricing backups util functions * Update Enable Backups drawer with dynamic pricing * Update Backups tab placeholder and confirmation dialog with dynamic pricing * Update mocks for now * Revert mock updates after rebase * Add test for backup price util function * Add DC-specific pricing to Linode Create flow * Update cached regions file to include Jakarta and Sao Paulo * Feature flag changes * Improve consistency with price variable names * Update tests * Renamed util function for consistency with #9570 * Added changeset: Add DC-specific pricing to Linode backups * Update backups drawer total cost util function to use FF * Address feedback: && over ternary * Address feedback: util and types * Missed a spot
1 parent a588aec commit 7ddcf3d

19 files changed

+398
-54
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add DC-specific pricing to Linode backups ([#9588](https://github.com/linode/manager/pull/9588))

packages/manager/src/cachedData/regions.json

+1-1
Large diffs are not rendered by default.

packages/manager/src/features/Backups/AutoEnroll.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const AutoEnroll = (props: AutoEnrollProps) => {
3535
>
3636
Backups pricing page
3737
</Link>
38+
.
3839
</Typography>
3940
</StyledDiv>
4041
}

packages/manager/src/features/Backups/BackupDrawer.tsx

+44-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { styled } from '@mui/material';
12
import Stack from '@mui/material/Stack';
23
import { useSnackbar } from 'notistack';
34
import * as React from 'react';
@@ -16,6 +17,7 @@ import { TableRow } from 'src/components/TableRow';
1617
import { TableRowError } from 'src/components/TableRowError/TableRowError';
1718
import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
1819
import { Typography } from 'src/components/Typography';
20+
import { useFlags } from 'src/hooks/useFlags';
1921
import {
2022
useAccountSettings,
2123
useMutateAccountSettings,
@@ -42,6 +44,8 @@ export const BackupDrawer = (props: Props) => {
4244
const { onClose, open } = props;
4345
const { enqueueSnackbar } = useSnackbar();
4446

47+
const flags = useFlags();
48+
4549
const {
4650
data: linodes,
4751
error: linodesError,
@@ -84,6 +88,13 @@ export const BackupDrawer = (props: Props) => {
8488

8589
const linodeCount = linodesWithoutBackups.length;
8690

91+
const backupsConfirmationHelperText = (
92+
<>
93+
Confirm to add backups to{' '}
94+
<strong>{pluralize('Linode', 'Linodes', linodeCount)}</strong>.
95+
</>
96+
);
97+
8798
const renderBackupsTable = () => {
8899
if (linodesLoading || typesLoading || accountSettingsLoading) {
89100
return <TableRowLoading columns={3} />;
@@ -137,7 +148,12 @@ all new Linodes will automatically be backed up.`
137148
};
138149

139150
return (
140-
<Drawer onClose={onClose} open={open} title="Enable All Backups">
151+
<Drawer
152+
onClose={onClose}
153+
open={open}
154+
title="Enable All Backups"
155+
wide={flags.dcSpecificPricing}
156+
>
141157
<Stack spacing={2}>
142158
<Typography variant="body1">
143159
Three backup slots are executed and rotated automatically: a daily
@@ -146,9 +162,8 @@ all new Linodes will automatically be backed up.`
146162
<Link to="https://www.linode.com/docs/platform/disk-images/linode-backup-service/">
147163
guide on Backups
148164
</Link>{' '}
149-
for more information on features and limitations. Confirm to add
150-
backups to{' '}
151-
<strong>{pluralize('Linode', 'Linodes', linodeCount)}</strong>.
165+
for more information on features and limitations.{' '}
166+
{!flags.dcSpecificPricing && backupsConfirmationHelperText}
152167
</Typography>
153168
{failedEnableBackupsCount > 0 && (
154169
<Box>
@@ -168,12 +183,22 @@ all new Linodes will automatically be backed up.`
168183
toggle={() => setShouldEnableAutoEnroll((prev) => !prev)}
169184
/>
170185
)}
171-
<Box>
186+
<StyledPricingBox>
187+
{flags.dcSpecificPricing && (
188+
<StyledTypography variant="h2">
189+
Total for {pluralize('Linode', 'Linodes', linodeCount)}:
190+
</StyledTypography>
191+
)}
192+
&nbsp;
172193
<DisplayPrice
194+
price={getTotalBackupsPrice({
195+
flags,
196+
linodes: linodesWithoutBackups,
197+
types: types ?? [],
198+
})}
173199
interval="mo"
174-
price={getTotalBackupsPrice(linodesWithoutBackups, types ?? [])}
175200
/>
176-
</Box>
201+
</StyledPricingBox>
177202
<ActionsPanel
178203
primaryButtonProps={{
179204
label: 'Confirm',
@@ -191,6 +216,7 @@ all new Linodes will automatically be backed up.`
191216
<TableRow>
192217
<TableCell>Label</TableCell>
193218
<TableCell>Plan</TableCell>
219+
{flags.dcSpecificPricing && <TableCell>Region</TableCell>}
194220
<TableCell>Price</TableCell>
195221
</TableRow>
196222
</TableHead>
@@ -200,3 +226,14 @@ all new Linodes will automatically be backed up.`
200226
</Drawer>
201227
);
202228
};
229+
230+
const StyledPricingBox = styled(Box, { label: 'StyledPricingBox' })(({}) => ({
231+
alignItems: 'center',
232+
display: 'flex',
233+
}));
234+
235+
const StyledTypography = styled(Typography, { label: 'StyledTypography' })(
236+
({ theme }) => ({
237+
color: theme.palette.text.primary,
238+
})
239+
);

packages/manager/src/features/Backups/BackupLinodeRow.test.tsx

+47-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers';
77
import { BackupLinodeRow } from './BackupLinodeRow';
88

99
describe('BackupLinodeRow', () => {
10-
it('should render linode, plan lable, and backups price', async () => {
10+
it('should render linode, plan label, and backups price', async () => {
1111
server.use(
1212
rest.get('*/linode/types/linode-type-test', (req, res, ctx) => {
1313
return res(
@@ -34,4 +34,50 @@ describe('BackupLinodeRow', () => {
3434
expect(await findByText('Linode Test Type')).toBeVisible();
3535
expect(await findByText('$12.99/mo')).toBeVisible();
3636
});
37+
38+
it('should render region and region-specific pricing if dcSpecificPricing feature flag is on', async () => {
39+
server.use(
40+
rest.get('*/linode/types/linode-type-test', (req, res, ctx) => {
41+
return res(
42+
ctx.json(
43+
linodeTypeFactory.build({
44+
addons: {
45+
backups: {
46+
price: {
47+
hourly: 0.004,
48+
monthly: 2.5,
49+
},
50+
region_prices: [
51+
{
52+
hourly: 0.0048,
53+
id: 'id-cgk',
54+
monthly: 3.57,
55+
},
56+
],
57+
},
58+
},
59+
label: 'Linode Test Type',
60+
})
61+
)
62+
);
63+
})
64+
);
65+
66+
const linode = linodeFactory.build({
67+
label: 'my-dc-pricing-linode-to-back-up',
68+
region: 'id-cgk',
69+
type: 'linode-type-test',
70+
});
71+
72+
const { findByText, getByText } = renderWithTheme(
73+
wrapWithTableBody(<BackupLinodeRow linode={linode} />, {
74+
flags: { dcSpecificPricing: true },
75+
})
76+
);
77+
78+
expect(getByText('my-dc-pricing-linode-to-back-up')).toBeVisible();
79+
expect(await findByText('Linode Test Type')).toBeVisible();
80+
expect(await findByText('Jakarta, ID')).toBeVisible();
81+
expect(await findByText('$3.57/mo')).toBeVisible();
82+
});
3783
});

packages/manager/src/features/Backups/BackupLinodeRow.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { Linode } from '@linode/api-v4';
1+
import { Linode, PriceObject } from '@linode/api-v4';
22
import * as React from 'react';
33

44
import { TableCell } from 'src/components/TableCell';
55
import { TableRow } from 'src/components/TableRow';
66
import { Typography } from 'src/components/Typography';
7+
import { useFlags } from 'src/hooks/useFlags';
8+
import { useRegionsQuery } from 'src/queries/regions';
79
import { useTypeQuery } from 'src/queries/types';
10+
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';
811

912
interface Props {
1013
error?: string;
@@ -14,6 +17,18 @@ interface Props {
1417
export const BackupLinodeRow = (props: Props) => {
1518
const { error, linode } = props;
1619
const { data: type } = useTypeQuery(linode.type ?? '', Boolean(linode.type));
20+
const { data: regions } = useRegionsQuery();
21+
const flags = useFlags();
22+
23+
const backupsMonthlyPrice: PriceObject['monthly'] = getMonthlyBackupsPrice({
24+
flags,
25+
region: linode.region,
26+
type,
27+
});
28+
29+
const regionLabel =
30+
regions?.find((r) => r.id === linode.region)?.label ?? linode.region;
31+
1732
return (
1833
<TableRow key={`backup-linode-${linode.id}`}>
1934
<TableCell parentColumn="Label">
@@ -33,8 +48,13 @@ export const BackupLinodeRow = (props: Props) => {
3348
<TableCell parentColumn="Plan">
3449
{type?.label ?? linode.type ?? 'Unknown'}
3550
</TableCell>
51+
{flags.dcSpecificPricing && (
52+
<TableCell parentColumn="Region">{regionLabel ?? 'Unknown'}</TableCell>
53+
)}
3654
<TableCell parentColumn="Price">
37-
{`$${type?.addons.backups.price.monthly?.toFixed(2) ?? 'Unknown'}/mo`}
55+
{backupsMonthlyPrice !== 0
56+
? `$${backupsMonthlyPrice?.toFixed(2)}/mo`
57+
: '$Unknown/mo'}
3858
</TableCell>
3959
</TableRow>
4060
);

packages/manager/src/features/Backups/utils.test.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,46 @@ describe('getTotalBackupsPrice', () => {
3434
addons: { backups: { price: { monthly: 2.5 } } },
3535
id: 'my-type',
3636
});
37-
expect(getTotalBackupsPrice(linodes, types)).toBe(7.5);
37+
expect(
38+
getTotalBackupsPrice({
39+
flags: { dcSpecificPricing: false },
40+
linodes,
41+
types,
42+
})
43+
).toBe(7.5);
44+
});
45+
46+
it('correctly calculates the total price with DC-specific pricing for Linode backups', () => {
47+
const basePriceLinodes = linodeFactory.buildList(2, { type: 'my-type' });
48+
const priceIncreaseLinode = linodeFactory.build({
49+
region: 'id-cgk',
50+
type: 'my-type',
51+
});
52+
const linodes = [...basePriceLinodes, priceIncreaseLinode];
53+
const types = linodeTypeFactory.buildList(1, {
54+
addons: {
55+
backups: {
56+
price: {
57+
hourly: 0.004,
58+
monthly: 2.5,
59+
},
60+
region_prices: [
61+
{
62+
hourly: 0.0048,
63+
id: 'id-cgk',
64+
monthly: 3.57,
65+
},
66+
],
67+
},
68+
},
69+
id: 'my-type',
70+
});
71+
expect(
72+
getTotalBackupsPrice({
73+
flags: { dcSpecificPricing: true },
74+
linodes,
75+
types,
76+
})
77+
).toBe(8.57);
3878
});
3979
});

packages/manager/src/features/Backups/utils.ts

+33-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,45 @@
11
import { enableBackups } from '@linode/api-v4';
22
import { useMutation, useQueryClient } from 'react-query';
33

4+
import { FlagSet } from 'src/featureFlags';
45
import { queryKey } from 'src/queries/linodes/linodes';
56
import { pluralize } from 'src/utilities/pluralize';
7+
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';
68

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

9-
export const getTotalBackupsPrice = (
10-
linodes: Linode[],
11-
types: LinodeType[]
12-
) => {
11+
export interface TotalBackupsPriceOptions {
12+
/**
13+
* Our feature flags so we can determined whether or not to add price increase.
14+
* @example { dcSpecificPricing: true }
15+
*/
16+
flags: FlagSet;
17+
/**
18+
* List of linodes without backups enabled
19+
*/
20+
linodes: Linode[];
21+
/**
22+
* List of types for the linodes without backups
23+
*/
24+
types: LinodeType[];
25+
}
26+
27+
export const getTotalBackupsPrice = ({
28+
flags,
29+
linodes,
30+
types,
31+
}: TotalBackupsPriceOptions) => {
1332
return linodes.reduce((prevValue: number, linode: Linode) => {
1433
const type = types.find((type) => type.id === linode.type);
15-
return prevValue + (type?.addons.backups.price.monthly ?? 0);
34+
35+
const backupsMonthlyPrice: PriceObject['monthly'] =
36+
getMonthlyBackupsPrice({
37+
flags,
38+
region: linode.region,
39+
type,
40+
}) || 0;
41+
42+
return prevValue + backupsMonthlyPrice;
1643
}, 0);
1744
};
1845

0 commit comments

Comments
 (0)