Skip to content

Commit

Permalink
feat: [M3-7036] DC specific transfer pool displays (#9620)
Browse files Browse the repository at this point in the history
* feat: [M3-6957] save work

* feat: [M3-7036] component cleanup

* feat: [M3-7036] save workk

* feat: [M3-7036] add region transfer pools PCT

* feat: [M3-7036] cleanup

* feat: [M3-7036] add usage to modal

* feat: [M3-7036] formatting

* feat: [M3-7036] flag splitting

* feat: [M3-7036] linear progress styling

* feat: [M3-7036] util test

* feat: [M3-7036] component tests

* feat: [M3-7036] cleanup

* feat: [M3-7036] better coverage

* feat: [M3-7036] test refactor

* feat: [M3-7036] test refactor 2

* feat: [M3-7036] NetworkTRansfer panel

* feat: [M3-7036] Naming conventions, component splitting and BarPercent

* feat: [M3-7036] Wrap up linode netowrk display transfer

* feat: [M3-7036] Test wrap up

* feat: [M3-7036] cleanup

* feat: [M3-7036] cleanup 2

* Added changeset: DC specific transfer pools and linode usage displays

* feat: [M3-7036] feedback 1

* feat: [M3-7036] feedback 2

* feat: [M3-7036] refactor doc links

* feat: [M3-7036] fix test

* feat: [M3-7036] fix Network Transfer History graph color

* feat: [M3-7036] small feedback 3
  • Loading branch information
abailly-akamai authored Sep 13, 2023
1 parent 02d0a7b commit 304ba45
Show file tree
Hide file tree
Showing 29 changed files with 1,601 additions and 422 deletions.
4 changes: 2 additions & 2 deletions packages/api-v4/src/linodes/info.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { API_ROOT } from '../constants';
import { NetworkUtilization, NetworkTransfer } from '../account/types';
import { RegionalNetworkUtilization, NetworkTransfer } from '../account/types';
import Request, { setMethod, setParams, setURL, setXFilter } from '../request';
import { Filter, Params, ResourcePage as Page } from '../types';
import { Kernel, LinodeType as Type, Stats } from './types';
Expand Down Expand Up @@ -52,7 +52,7 @@ export const getLinodeStatsByDate = (
* @param linodeId { number } The id of the Linode to retrieve network transfer information for.
*/
export const getLinodeTransfer = (linodeId: number) =>
Request<NetworkUtilization>(
Request<RegionalNetworkUtilization>(
setURL(
`${API_ROOT}/linode/instances/${encodeURIComponent(linodeId)}/transfer`
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

DC specific transfer pools and linode usage displays ([#9620](https://github.com/linode/manager/pull/9620))
5 changes: 3 additions & 2 deletions packages/manager/src/components/BarPercent/BarPercent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ const StyledLinearProgress = styled(LinearProgress, {
shouldForwardProp: (prop) => isPropValid(['rounded'], prop),
})<Partial<BarPercentProps>>(({ theme, ...props }) => ({
'& .MuiLinearProgress-bar2Buffer': {
backgroundColor: '#99ec79',
backgroundColor: '#5ad865',
},
'& .MuiLinearProgress-barColorPrimary': {
backgroundColor: '#5ad865',
// Increase contrast if we have a buffer bar
backgroundColor: props.valueBuffer ? '#1CB35C' : '#5ad865',
},
'& .MuiLinearProgress-dashed': {
display: 'none',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// =================================================================================
// This is a copy of the original TransferDisplay test, which is getting deprecated.
// It can be safely deleted once the new TransferDisplay component
// is fully rolled out and the dcSpecificPricing flag is cleaned up.
// ==================================================================================

import { fireEvent } from '@testing-library/react';
import React from 'react';

import { rest, server } from 'src/mocks/testServer';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { TransferDisplay } from './TransferDisplay';

const MockData = {
billable: 0,
quota: 0,
used: 0,
};

const transferDisplayPercentageSubstring = /You have used \d+\.\d\d%/;
const transferDisplayButtonSubstring = /Monthly Network Transfer Pool/;
const docsLink = 'https://www.linode.com/docs/guides/network-transfer-quota/';

describe('TransferDisplay', () => {
it('renders transfer display text and opens the transfer dialog, with GB data stats, on click', async () => {
server.use(
rest.get('*/account/transfer', (req, res, ctx) => {
return res(
ctx.json({
billable: 0,
quota: 11347,
used: 50,
})
);
})
);

const { findByText, getByTestId } = renderWithTheme(<TransferDisplay />);
const transferButton = await findByText(transferDisplayButtonSubstring, {
exact: false,
});

expect(transferButton).toBeInTheDocument();
expect(
await findByText(transferDisplayPercentageSubstring, { exact: false })
).toBeInTheDocument();
fireEvent.click(transferButton);

const transferDialog = getByTestId('drawer');
expect(transferDialog.innerHTML).toMatch(/GB/);
});

it('renders transfer display text with a percentage of 0.00% if no usage', async () => {
server.use(
rest.get('*/account/transfer', (req, res, ctx) => {
return res(ctx.json(MockData));
})
);

const { findByText } = renderWithTheme(<TransferDisplay />);
const usage = await findByText(transferDisplayPercentageSubstring, {
exact: false,
});

expect(usage.innerHTML).toMatch(/0.00%/);
});

it('renders transfer display dialog without usage or quota data if no quota/resources', async () => {
server.use(
rest.get('*/account/transfer', (req, res, ctx) => {
return res(ctx.json(MockData));
})
);

const { findByText, getByTestId } = renderWithTheme(<TransferDisplay />);
const transferButton = await findByText(transferDisplayButtonSubstring);
fireEvent.click(transferButton);

const transferDialog = getByTestId('drawer');
expect(transferDialog.innerHTML).toMatch(
/Your monthly network transfer will be shown when you create a resource./
);
expect(transferDialog.innerHTML).not.toMatch(/GB/);
});

it('renders the transfer display dialog with an accessible docs link', async () => {
server.use(
rest.get('*/account/transfer', (req, res, ctx) => {
return res(ctx.json(MockData));
})
);

const { findByText, getByRole } = renderWithTheme(<TransferDisplay />);
const transferButton = await findByText(transferDisplayButtonSubstring);
fireEvent.click(transferButton);

expect(getByRole('link')).toHaveAttribute('href', docsLink);
expect(getByRole('link').getAttribute('aria-label'));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// ======================================================================================
// This is a copy of the original TransferDisplay component, which is getting deprecated.
// It can be safely deleted once the new TransferDisplay component
// is fully rolled out and the dcSpecificPricing flag is cleaned up.
// ======================================================================================

import Grid from '@mui/material/Unstable_Grid2';
import { Theme } from '@mui/material/styles';
import { DateTime } from 'luxon';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';

import BarPercent from 'src/components/BarPercent';
import { Dialog } from 'src/components/Dialog/Dialog';
import { Link } from 'src/components/Link';
import { Typography } from 'src/components/Typography';
import { useAccountTransfer } from 'src/queries/accountTransfer';

const useStyles = makeStyles()((theme: Theme) => ({
link: {
marginTop: theme.spacing(1),
},
openModalButton: {
...theme.applyLinkStyles,
},
paddedDocsText: {
[theme.breakpoints.up('md')]: {
paddingRight: theme.spacing(3), // Prevents link text from being split onto two lines.
},
},
paper: {
padding: theme.spacing(3),
},
poolUsageProgress: {
'& .MuiLinearProgress-root': {
borderRadius: 1,
},
marginBottom: theme.spacing(0.5),
},
proratedNotice: {
marginBottom: theme.spacing(1),
marginTop: theme.spacing(1),
},
root: {
margin: 'auto',
textAlign: 'center',
[theme.breakpoints.down('md')]: {
width: '85%',
},
width: '100%',
},
}));

export interface Props {
spacingTop?: number;
}

export const LegacyTransferDisplay = React.memo(({ spacingTop }: Props) => {
const { classes } = useStyles();

const [modalOpen, setModalOpen] = React.useState(false);
const { data, isError, isLoading } = useAccountTransfer();
const quota = data?.quota ?? 0;
const used = data?.used ?? 0;

// Usage percentage should not be 100% if there has been no usage or usage has not exceeded quota.
const poolUsagePct =
used < quota ? (used / quota) * 100 : used === 0 ? 0 : 100;

if (isError) {
// We may want to add an error state for this but I think that would clutter
// up the display.
return null;
}

return (
<>
<Typography
className={classes.root}
style={{ marginTop: spacingTop ?? 8 }}
>
{isLoading ? (
'Loading transfer data...'
) : (
<>
You have used {poolUsagePct.toFixed(poolUsagePct < 1 ? 2 : 0)}% of
your
{` `}
<button
className={classes.openModalButton}
onClick={() => setModalOpen(true)}
>
Monthly Network Transfer Pool
</button>
.
</>
)}
</Typography>
<TransferDialog
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
poolUsagePct={poolUsagePct}
quota={quota}
used={used}
/>
</>
);
});

export const getDaysRemaining = () =>
Math.floor(
DateTime.local()
.setZone('America/New_York')
.endOf('month')
.diffNow('days')
.toObject().days ?? 0
);

// =============================================================================
// Dialog
// =============================================================================
interface DialogProps {
isOpen: boolean;
onClose: () => void;
poolUsagePct: number;
quota: number;
used: number;
}

export const TransferDialog = React.memo((props: DialogProps) => {
const { isOpen, onClose, poolUsagePct, quota, used } = props;
const { classes } = useStyles();
const daysRemainingInMonth = getDaysRemaining();
// Don't display usage, quota, or bar percent if the network transfer pool is empty (e.g. account has no resources).
const isEmptyPool = quota === 0;
const transferQuotaDocsText =
used === 0 ? (
<span className={classes.paddedDocsText}>
Compute instances, NodeBalancers, and Object Storage include network
transfer.
</span>
) : (
'View products and services that include network transfer, and learn how to optimize network usage to avoid billing surprises.'
);

return (
<Dialog
classes={{ paper: classes.paper }}
fullWidth
maxWidth="sm"
onClose={onClose}
open={isOpen}
title="Monthly Network Transfer Pool"
>
<Grid
container
justifyContent="space-between"
spacing={2}
style={{ marginBottom: 0 }}
>
<Grid style={{ marginRight: 10 }}>
{!isEmptyPool ? (
<Typography>{used} GB Used</Typography>
) : (
<Typography>
Your monthly network transfer will be shown when you create a
resource.
</Typography>
)}
</Grid>
<Grid>
{!isEmptyPool ? (
<Typography>
{quota >= used ? (
<span>{quota - used} GB Available</span>
) : (
<span>
{(quota - used).toString().replace(/\-/, '')} GB Over Quota
</span>
)}
</Typography>
) : null}
</Grid>
</Grid>
{!isEmptyPool ? (
<BarPercent
className={classes.poolUsageProgress}
max={100}
rounded
value={Math.ceil(poolUsagePct)}
/>
) : null}

<Typography className={classes.proratedNotice}>
<strong>
Your account&rsquo;s monthly network transfer allotment will reset in{' '}
{daysRemainingInMonth} days.
</strong>
</Typography>
<Typography className={classes.proratedNotice}>
Your account&rsquo;s network transfer pool adds up all the included
transfer associated with active Linode services on your account and is
prorated based on service creation.
</Typography>
<div className={classes.link}>
<Typography>
{transferQuotaDocsText}{' '}
<Link
aria-label="Learn more – link opens in a new tab"
to="https://www.linode.com/docs/guides/network-transfer-quota/"
>
Learn more.
</Link>
</Typography>
</div>
</Dialog>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { styled } from '@mui/material/styles';

import { Box } from 'src/components/Box';

export const StyledTransferDisplayContainer = styled(Box, {
label: 'StyledTransferDisplayTypography',
})(({ theme }) => ({
margin: 'auto',
textAlign: 'center',
[theme.breakpoints.down('md')]: {
width: '85%',
},
width: '100%',
}));
Loading

0 comments on commit 304ba45

Please sign in to comment.