diff --git a/packages/manager/.changeset/pr-10228-changed-1709051214975.md b/packages/manager/.changeset/pr-10228-changed-1709051214975.md new file mode 100644 index 00000000000..f648bd4f91d --- /dev/null +++ b/packages/manager/.changeset/pr-10228-changed-1709051214975.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Disable 512GB Plans ([#10228](https://github.com/linode/manager/pull/10228)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index bc5b2e91c21..9d89090ff43 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -983,6 +983,10 @@ describe('LKE cluster updates for DC-specific prices', () => { .findByTitle(`Add a Node Pool: ${mockCluster.label}`) .should('be.visible') .within(() => { + cy.findByText('Shared CPU') + .should('be.visible') + .should('be.enabled') + .click(); cy.findByText('Linode 0 GB') .should('be.visible') .closest('tr') @@ -1209,6 +1213,10 @@ describe('LKE cluster updates for DC-specific prices', () => { .findByTitle(`Add a Node Pool: ${mockCluster.label}`) .should('be.visible') .within(() => { + cy.findByText('Shared CPU') + .should('be.visible') + .should('be.enabled') + .click(); cy.findByText('Linode 2 GB') .should('be.visible') .closest('tr') diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index bceb2de127d..7328a4d545f 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -1,5 +1,5 @@ import Grid from '@mui/material/Unstable_Grid2'; -import { styled } from '@mui/material/styles'; +import { Theme, styled } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import * as React from 'react'; @@ -78,7 +78,7 @@ export interface SelectionCardProps { /** * Optional styles to apply to the grid of the card. */ - sxGrid?: SxProps; + sxGrid?: SxProps; /** * Optional styles to apply to the tooltip of the card. */ @@ -86,7 +86,7 @@ export interface SelectionCardProps { /** * Optional text to set in a tooltip when hovering over the card. */ - tooltip?: string; + tooltip?: JSX.Element | string; } /** @@ -112,6 +112,7 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => { sxCardBaseIcon, sxCardBaseSubheading, sxGrid, + sxTooltip, tooltip, } = props; @@ -146,6 +147,7 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => { const cardGrid = ( { if (tooltip) { return ( - + {cardGrid} ); diff --git a/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx b/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx index 45ca372bdf8..caa03c690b2 100644 --- a/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx +++ b/packages/manager/src/components/SingleTextFieldForm/SingleTextFieldForm.tsx @@ -93,6 +93,7 @@ export const SingleTextFieldForm = React.memo((props: Props) => { /> { error={errors.type} header="Choose a Plan" isCreate + regionsData={regionsData} selectedId={values.type} + selectedRegionID={values.region} types={displayTypes} /> diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.test.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.test.tsx index d3598bc8e27..0000dc08685 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.test.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.test.tsx @@ -9,7 +9,18 @@ import { KubernetesPlanContainerProps, } from './KubernetesPlanContainer'; -const plans = extendedTypeFactory.buildList(2); +import type { TypeWithAvailability } from 'src/features/components/PlansPanel/types'; + +const plans: TypeWithAvailability[] = [ + { + ...extendedTypeFactory.build(), + isLimitedAvailabilityPlan: false, + }, + { + ...extendedTypeFactory.build(), + isLimitedAvailabilityPlan: true, + }, +]; const props: KubernetesPlanContainerProps = { getTypeCount: vi.fn(), diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx index a8715ad58a4..d8c302f7f5b 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx @@ -9,14 +9,12 @@ import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; -import { getIsPlanSoldOut } from 'src/features/components/PlansPanel/utils'; -import { useFlags } from 'src/hooks/useFlags'; -import { useRegionsAvailabilityQuery } from 'src/queries/regions'; -import { ExtendedType } from 'src/utilities/extendType'; import { PLAN_SELECTION_NO_REGION_SELECTED_MESSAGE } from 'src/utilities/pricing/constants'; import { KubernetesPlanSelection } from './KubernetesPlanSelection'; +import type { TypeWithAvailability } from 'src/features/components/PlansPanel/types'; + const tableCells = [ { cellName: 'Plan', center: false, noWrap: false, testId: 'plan' }, { cellName: 'Monthly', center: false, noWrap: false, testId: 'monthly' }, @@ -32,7 +30,7 @@ export interface KubernetesPlanContainerProps { getTypeCount: (planId: string) => number; onAdd?: (key: string, value: number) => void; onSelect: (key: string) => void; - plans: ExtendedType[]; + plans: TypeWithAvailability[]; selectedId?: string; selectedRegionId?: string; updatePlanCount: (planId: string, newCount: number) => void; @@ -51,28 +49,19 @@ export const KubernetesPlanContainer = ( selectedRegionId, updatePlanCount, } = props; - const flags = useFlags(); - const { data: regionAvailabilities } = useRegionsAvailabilityQuery( - selectedRegionId || '', - Boolean(flags.soldOutChips) && selectedRegionId !== undefined - ); const shouldDisplayNoRegionSelectedMessage = !selectedRegionId; const renderPlanSelection = React.useCallback(() => { return plans.map((plan, id) => { - const isPlanSoldOut = getIsPlanSoldOut({ - plan, - regionAvailabilities, - selectedRegionId, - }); - return ( { it('should not display an error message for $0 regions', () => { const propsWithRegionZeroPrice = { ...props, - type: extendedTypeFactory.build({ - region_prices: [ - { - hourly: 0, - id: 'id-cgk', - monthly: 0, - }, - ], - }), + type: { + ...extendedTypeFactory.build({ + region_prices: [ + { + hourly: 0, + id: 'id-cgk', + monthly: 0, + }, + ], + }), + isLimitedAvailabilityPlan: false, + }, }; const { container } = renderWithTheme( wrapWithTableBody( @@ -107,53 +115,112 @@ describe('KubernetesPlanSelection (table, desktop view)', () => { ).not.toBeInTheDocument(); }); - describe('KubernetesPlanSelection (cards, mobile view)', () => { - beforeAll(() => { - resizeScreenSize(breakpoints.values.sm); - }); + it('shows limited availability messaging for 512 GB plans', async () => { + const bigPlanType: TypeWithAvailability = { + ...extendedTypeFactory.build({ + heading: 'Dedicated 512 GB', + label: 'Dedicated 512GB', + }), + isLimitedAvailabilityPlan: false, + }; - it('displays the plan header label, monthly and hourly price, RAM, CPUs, and storage', async () => { - const { getByText } = renderWithTheme( - - ); - - expect(getByText(planHeader)).toBeInTheDocument(); - expect( - getByText(`${baseMonthlyPrice}/mo`, { exact: false }) - ).toBeInTheDocument(); - expect( - getByText(`${baseHourlyPrice}/hr`, { exact: false }) - ).toBeInTheDocument(); - expect(getByText(`${cpu} CPU`, { exact: false })).toBeInTheDocument(); - expect( - getByText(`${storage} Storage`, { exact: false }) - ).toBeInTheDocument(); - expect(getByText(`${ram} RAM`, { exact: false })).toBeInTheDocument(); - }); + const { getByRole, getByTestId, getByText } = renderWithTheme( + wrapWithTableBody( + , + { flags: { disableLargestGbPlans: true } } + ) + ); - it('displays DC-specific prices in a region with a price increase', async () => { - const { getByText } = renderWithTheme( - - ); - - expect( - getByText(`${regionMonthlyPrice}/mo`, { exact: false }) - ).toBeInTheDocument(); - expect( - getByText(`${regionHourlyPrice}/hr`, { exact: false }) - ).toBeInTheDocument(); + const button = getByTestId('limited-availability'); + fireEvent.mouseOver(button); + + await waitFor(() => { + expect(getByRole('tooltip')).toBeInTheDocument(); }); - it('shows a chip if plan is sold out', () => { - const { getByLabelText } = renderWithTheme( - - ); + expect(getByText(LIMITED_AVAILABILITY_TEXT)).toBeVisible(); + }); +}); + +describe('KubernetesPlanSelection (cards, mobile view)', () => { + beforeAll(() => { + resizeScreenSize(breakpoints.values.sm); + }); + + it('displays the plan header label, monthly and hourly price, RAM, CPUs, and storage', async () => { + const { getByText } = renderWithTheme( + + ); - expect(getByLabelText(PLAN_IS_SOLD_OUT_COPY)).toBeInTheDocument(); + expect(getByText(planHeader)).toBeInTheDocument(); + expect( + getByText(`${baseMonthlyPrice}/mo`, { exact: false }) + ).toBeInTheDocument(); + expect( + getByText(`${baseHourlyPrice}/hr`, { exact: false }) + ).toBeInTheDocument(); + expect(getByText(`${cpu} CPU`, { exact: false })).toBeInTheDocument(); + expect( + getByText(`${storage} Storage`, { exact: false }) + ).toBeInTheDocument(); + expect(getByText(`${ram} RAM`, { exact: false })).toBeInTheDocument(); + }); + + it('displays DC-specific prices in a region with a price increase', async () => { + const { getByText } = renderWithTheme( + + ); + + expect( + getByText(`${regionMonthlyPrice}/mo`, { exact: false }) + ).toBeInTheDocument(); + expect( + getByText(`${regionHourlyPrice}/hr`, { exact: false }) + ).toBeInTheDocument(); + }); + + it('shows limited availability messaging', async () => { + const { getByRole, getByTestId, getByText } = renderWithTheme( + + ); + + const selectionCard = getByTestId('selection-card'); + fireEvent.mouseOver(selectionCard); + + await waitFor(() => { + expect(getByRole('tooltip')).toBeInTheDocument(); }); + + expect(getByText(LIMITED_AVAILABILITY_TEXT)).toBeVisible(); + }); + + it('is disabled for 512 GB plans', () => { + const bigPlanType: TypeWithAvailability = { + ...extendedTypeFactory.build({ + heading: 'Dedicated 512 GB', + label: 'Dedicated 512GB', + }), + isLimitedAvailabilityPlan: false, + }; + + const { getByTestId } = renderWithTheme( + , + { flags: { disableLargestGbPlans: true } } + ); + + const selectionCard = getByTestId('selection-card'); + expect(selectionCard).toBeDisabled(); }); }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx index d76a5408b74..b5d39d34f5c 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanSelection.tsx @@ -1,19 +1,22 @@ import { PriceObject } from '@linode/api-v4'; import { Region } from '@linode/api-v4/lib/regions'; -import Grid from '@mui/material/Unstable_Grid2'; +import HelpOutline from '@mui/icons-material/HelpOutline'; import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; +import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { Chip } from 'src/components/Chip'; import { EnhancedNumberInput } from 'src/components/EnhancedNumberInput/EnhancedNumberInput'; import { Hidden } from 'src/components/Hidden'; +import { IconButton } from 'src/components/IconButton'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { TableCell } from 'src/components/TableCell'; import { Tooltip } from 'src/components/Tooltip'; -import { PLAN_IS_SOLD_OUT_COPY } from 'src/constants'; +import { LIMITED_AVAILABILITY_TEXT } from 'src/features/components/PlansPanel/constants'; import { StyledDisabledTableRow } from 'src/features/components/PlansPanel/PlansPanel.styles'; -import { ExtendedType } from 'src/utilities/extendType'; +import { useFlags } from 'src/hooks/useFlags'; import { PRICE_ERROR_TOOLTIP_TEXT, UNKNOWN_PRICE, @@ -22,16 +25,18 @@ import { renderMonthlyPriceToCorrectDecimalPlace } from 'src/utilities/pricing/d import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; +import type { TypeWithAvailability } from 'src/features/components/PlansPanel/types'; + export interface KubernetesPlanSelectionProps { disabled?: boolean; getTypeCount: (planId: string) => number; idx: number; - isPlanSoldOut: boolean; + isLimitedAvailabilityPlan: boolean; onAdd?: (key: string, value: number) => void; onSelect: (key: string) => void; selectedId?: string; selectedRegionId?: Region['id']; - type: ExtendedType; + type: TypeWithAvailability; updatePlanCount: (planId: string, newCount: number) => void; } @@ -42,7 +47,7 @@ export const KubernetesPlanSelection = ( disabled, getTypeCount, idx, - isPlanSoldOut, + isLimitedAvailabilityPlan, onAdd, onSelect, selectedId, @@ -51,8 +56,15 @@ export const KubernetesPlanSelection = ( updatePlanCount, } = props; - const count = getTypeCount(type.id); + const flags = useFlags(); + // Determine if the plan should be disabled solely due to being a 512GB plan + const disabled512GbPlan = + type.label.includes('512GB') && + Boolean(flags.disableLargestGbPlans) && + !disabled; + const isDisabled = disabled || isLimitedAvailabilityPlan || disabled512GbPlan; + const count = getTypeCount(type.id); const price: PriceObject | undefined = getLinodeRegionPrice( type, selectedRegionId @@ -70,14 +82,19 @@ export const KubernetesPlanSelection = ( updatePlanCount(type.id, newCount)} value={count} /> {onAdd && (