diff --git a/packages/manager/.changeset/pr-11209-added-1730788980490.md b/packages/manager/.changeset/pr-11209-added-1730788980490.md new file mode 100644 index 00000000000..8973630d4ca --- /dev/null +++ b/packages/manager/.changeset/pr-11209-added-1730788980490.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +New GPUv2 egress transfer helpers ([#11209](https://github.com/linode/manager/pull/11209)) diff --git a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts index 94827d5d9cf..99255a0241c 100644 --- a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts @@ -12,6 +12,7 @@ import { mockGetRegionAvailability, } from 'support/intercepts/regions'; import { mockGetLinodeTypes } from 'support/intercepts/linodes'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; const mockRegions = [ regionFactory.build({ @@ -356,11 +357,18 @@ describe('displays specific linode plans for GPU', () => { mockGetRegionAvailability(mockRegions[0].id, mockRegionAvailability).as( 'getRegionAvailability' ); + mockAppendFeatureFlags({ + gpuv2: { + transferBanner: true, + planDivider: true, + egressBanner: true, + }, + }).as('getFeatureFlags'); }); it('Should render divided tables when GPU divider enabled', () => { cy.visitWithLogin('/linodes/create'); - + cy.wait(['@getRegions', '@getLinodeTypes', '@getFeatureFlags']); ui.regionSelect.find().click(); ui.regionSelect.findItemByRegionLabel(mockRegions[0].label).click(); @@ -368,7 +376,7 @@ describe('displays specific linode plans for GPU', () => { // Should display two separate tables cy.findByText('GPU').click(); cy.get(linodePlansPanel).within(() => { - cy.findAllByRole('alert').should('have.length', 2); + cy.findAllByRole('alert').should('have.length', 3); cy.get(notices.unavailable).should('be.visible'); cy.findByRole('table', { diff --git a/packages/manager/src/components/TableCell/TableCell.tsx b/packages/manager/src/components/TableCell/TableCell.tsx index 159b10fcdde..f30c4a116dc 100644 --- a/packages/manager/src/components/TableCell/TableCell.tsx +++ b/packages/manager/src/components/TableCell/TableCell.tsx @@ -1,13 +1,12 @@ -import { - default as _TableCell, - TableCellProps as _TableCellProps, -} from '@mui/material/TableCell'; -import { Theme } from '@mui/material/styles'; +import { default as _TableCell } from '@mui/material/TableCell'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; import { TooltipIcon } from 'src/components/TooltipIcon'; +import type { Theme } from '@mui/material/styles'; +import type { TableCellProps as _TableCellProps } from '@mui/material/TableCell'; + const useStyles = makeStyles()((theme: Theme) => ({ actionCell: { textAlign: 'right', @@ -106,7 +105,11 @@ export const TableCell = (props: TableCellProps) => { [classes.root]: true, [classes.sortable]: sortable, // hide the cell at small breakpoints if it's empty with no parent column - emptyCell: !parentColumn && !props.children, + emptyCell: + (!parentColumn && !props.children) || + (!parentColumn && + Array.isArray(props.children) && + !props.children[0]), }, className )} diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 9a92408132d..edbf8292360 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -72,6 +72,7 @@ export interface CloudPulseResourceTypeMapFlag { interface gpuV2 { egressBanner: boolean; planDivider: boolean; + transferBanner: boolean; } interface DesignUpdatesBannerFlag extends BaseFeatureFlag { diff --git a/packages/manager/src/features/components/PlansPanel/PlanContainer.styles.ts b/packages/manager/src/features/components/PlansPanel/PlanContainer.styles.ts index 6260a155855..dac60cbc026 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanContainer.styles.ts +++ b/packages/manager/src/features/components/PlansPanel/PlanContainer.styles.ts @@ -12,14 +12,15 @@ interface StyledTableCellPropsProps extends TableCellProps { export const StyledTable = styled(Table, { label: 'StyledTable', -})(({ theme }) => ({ +})({ overflowX: 'hidden', -})); +}); export const StyledTableCell = styled(TableCell, { label: 'StyledTableCell', shouldForwardProp: omittedProps(['isPlanCell']), })(({ theme, ...props }) => ({ + ...(props.isPlanCell && { width: '30%' }), '&.emptyCell': { borderRight: 'none', }, diff --git a/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx b/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx index 512bcbb91fb..b1201e0a3d0 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanContainer.tsx @@ -1,4 +1,3 @@ -import { LinodeTypeClass } from '@linode/api-v4/lib/linodes'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { useLocation } from 'react-router-dom'; @@ -14,6 +13,7 @@ import { PlanSelectionTable } from './PlanSelectionTable'; import type { PlanWithAvailability } from './types'; import type { Region } from '@linode/api-v4'; +import type { LinodeTypeClass } from '@linode/api-v4/lib/linodes'; export interface PlanContainerProps { allDisabledPlans: PlanWithAvailability[]; @@ -50,7 +50,8 @@ export const PlanContainer = (props: PlanContainerProps) => { // Show the Transfer column if, for any plan, the api returned data and we're not in the Database Create flow const showTransfer = - showLimits && plans.some((plan: PlanWithAvailability) => plan.transfer); + showLimits && + plans.some((plan: PlanWithAvailability) => plan.transfer !== undefined); // Show the Network throughput column if, for any plan, the api returned data (currently Bare Metal does not) const showNetwork = @@ -198,6 +199,7 @@ export const PlanContainer = (props: PlanContainerProps) => { } key={`plan-filter-${idx}`} planFilter={table.planFilter} + plans={plans} showNetwork={showNetwork} showTransfer={showTransfer} /> diff --git a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx index 56c159989a0..422fa0f9a19 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx @@ -45,6 +45,7 @@ export const PlanInformation = (props: PlanInformationProps) => { return Boolean(disabledClasses?.includes(thisClass)); }; const showGPUEgressBanner = Boolean(useFlags().gpuv2?.egressBanner); + const showTransferBanner = Boolean(useFlags().gpuv2?.transferBanner); return ( <> @@ -68,6 +69,23 @@ export const PlanInformation = (props: PlanInformationProps) => { )} + {showTransferBanner && ( + + theme.font.bold} + fontSize="1rem" + > + Some plans do not include bundled network transfer. If the + transfer allotment is 0, all outbound network transfer is + subject to standard charges. +
+ + Learn more about transfer costs + + . +
+
+ )} { {showTransfer ? ( - {plan.transfer ? <>{plan.transfer / 1000} TB : ''} + {plan.transfer !== undefined ? ( + <>{plan.transfer / 1000} TB + ) : ( + '' + )} ) : null} {showNetwork ? ( diff --git a/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx b/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx index ee13cbe28b0..4cd06f4a678 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanSelectionTable.tsx @@ -4,10 +4,13 @@ import { TableBody } from 'src/components/TableBody'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; +import { TooltipIcon } from 'src/components/TooltipIcon'; +import { useFlags } from 'src/hooks/useFlags'; import { PLAN_SELECTION_NO_REGION_SELECTED_MESSAGE } from 'src/utilities/pricing/constants'; import { StyledTable, StyledTableCell } from './PlanContainer.styles'; -import { PlanWithAvailability } from './types'; + +import type { PlanWithAvailability } from './types'; interface PlanSelectionFilterOptionsTable { header?: string; @@ -17,6 +20,7 @@ interface PlanSelectionFilterOptionsTable { interface PlanSelectionTableProps { filterOptions?: PlanSelectionFilterOptionsTable; planFilter?: (plan: PlanWithAvailability) => boolean; + plans?: PlanWithAvailability[]; renderPlanSelection: ( filterOptions?: PlanSelectionFilterOptionsTable | undefined ) => React.JSX.Element[]; @@ -45,11 +49,26 @@ const tableCells = [ export const PlanSelectionTable = (props: PlanSelectionTableProps) => { const { filterOptions, + plans, renderPlanSelection, shouldDisplayNoRegionSelectedMessage, showNetwork: shouldShowNetwork, showTransfer: shouldShowTransfer, } = props; + const flags = useFlags(); + + const showTransferTooltip = React.useCallback( + (cellName: string) => + plans?.some((plan) => { + return ( + flags.gpuv2?.transferBanner && + plan.class === 'gpu' && + filterOptions?.header?.includes('Ada') && + cellName === 'Transfer' + ); + }), + [plans, filterOptions, flags.gpuv2] + ); return ( { {isPlanCell && filterOptions?.header ? filterOptions?.header : cellName} + {showTransferTooltip(cellName) && ( + + )} ); })} diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 2d092465043..6b66bd9e68c 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -384,7 +384,17 @@ const nanodeType = linodeTypeFactory.build({ id: 'g6-nanode-1' }); const standardTypes = linodeTypeFactory.buildList(7); const dedicatedTypes = dedicatedTypeFactory.buildList(7); const proDedicatedType = proDedicatedTypeFactory.build(); - +const gpuTypesAda = linodeTypeFactory.buildList(7, { + class: 'gpu', + gpus: 5, + label: 'Ada Lovelace', + transfer: 0, +}); +const gpuTypesRX = linodeTypeFactory.buildList(7, { + class: 'gpu', + gpus: 1, + transfer: 5000, +}); const proxyAccountUser = accountUserFactory.build({ email: 'partner@proxy.com', last_login: null, @@ -578,6 +588,8 @@ export const handlers = [ nanodeType, ...standardTypes, ...dedicatedTypes, + ...gpuTypesAda, + ...gpuTypesRX, proDedicatedType, ]) );