Skip to content
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": Changed
---

Implement GPUv2 Plan Divider & Cleanup/Consolidate PlanSelection components ([#10407](https://github.com/linode/manager/pull/10407))
118 changes: 84 additions & 34 deletions packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// TODO: Cypress
// Move this to cypress component testing once the setup is complete - see https://github.com/linode/manager/pull/10134
import { fbtClick } from 'support/helpers';
import { ui } from 'support/ui';
import {
regionFactory,
Expand All @@ -13,6 +12,13 @@ import {
mockGetRegionAvailability,
} from 'support/intercepts/regions';
import { mockGetLinodeTypes } from 'support/intercepts/linodes';
import {
mockAppendFeatureFlags,
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import { makeFeatureFlagData } from 'support/util/feature-flags';

import type { Flags } from 'src/featureFlags';

const mockRegions = [
regionFactory.build({
Expand Down Expand Up @@ -72,6 +78,11 @@ const mockGPUType = [
label: 'gpu-1',
class: 'gpu',
}),
linodeTypeFactory.build({
id: 'gpu-2',
label: 'gpu-2 Ada',
class: 'gpu',
}),
];

const mockLinodeTypes = [
Expand Down Expand Up @@ -99,7 +110,7 @@ const k8PlansPanel = '[data-qa-tp="Add Node Pools"]';
const planSelectionTable = 'List of Linode Plans';

const notices = {
limitedAvailability: '[data-testid="limited-availability"]',
limitedAvailability: '[data-testid="disabled-plan-tooltip"]',
unavailable: '[data-testid="notice-error"]',
};

Expand Down Expand Up @@ -136,9 +147,12 @@ describe('displays linode plans panel based on availability', () => {
cy.findAllByRole('row').should('have.length', 5);
cy.get('[id="dedicated-1"]').should('be.enabled');
cy.get('[id="dedicated-2"]').should('be.enabled');
cy.get(
'[aria-label="dedicated-3 - This plan has limited deployment availability."]'
);
cy.get('[id="dedicated-3"]').should('be.disabled');
cy.get('[id="g6-dedicated-64"]').should('be.disabled');
cy.findAllByTestId('limited-availability').should('have.length', 2);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 2);
});
});

Expand All @@ -147,7 +161,7 @@ describe('displays linode plans panel based on availability', () => {
// Should contain 3 plans (4 rows including the header row)
// Should have 0 disabled plan
// Should have no tooltip for the disabled plan
fbtClick('Shared CPU');
cy.findByText('Shared CPU').click();
cy.get(linodePlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 0);

Expand All @@ -156,7 +170,7 @@ describe('displays linode plans panel based on availability', () => {
cy.get('[id="shared-1"]').should('be.enabled');
cy.get('[id="shared-2"]').should('be.enabled');
cy.get('[id="shared-3"]').should('be.enabled');
cy.findAllByTestId('limited-availability').should('have.length', 0);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 0);
});
});

Expand All @@ -165,32 +179,15 @@ describe('displays linode plans panel based on availability', () => {
// Should contain 1 plan (2 rows including the header row)
// Should have one disabled plan
// Should have tooltip for the disabled plan (more than half disabled plans in the panel, but only one plan)
fbtClick('High Memory');
cy.findByText('High Memory').click();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fbtClick is now marked as deprecated

cy.get(linodePlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.limitedAvailability).should('be.visible');

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
cy.get('[id="highmem-1"]').should('be.disabled');
cy.findAllByTestId('limited-availability').should('have.length', 1);
});
});

// GPU tab
// Should have the unavailable notice
// Should contain 1 plan (2 rows including the header row)
// Should have its panel disabled
// Should not have tooltip for the disabled plan (not needed on disabled panels)
fbtClick('GPU');
cy.get(linodePlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
cy.get('[id="gpu-1"]').should('be.disabled');
cy.findAllByTestId('limited-availability').should('have.length', 0);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 1);
});
});

Expand All @@ -200,15 +197,15 @@ describe('displays linode plans panel based on availability', () => {
// Should contain 1 plan (2 rows including the header row)
// Should have its whole panel disabled
// Should not have tooltip for the disabled plan (not needed on disabled panels)
fbtClick('Premium CPU');
cy.findByText('Premium CPU').click();
cy.get(linodePlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
cy.get('[id="g7-premium-64"]').should('be.disabled');
cy.findAllByTestId('limited-availability').should('have.length', 0);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 0);
});
});
});
Expand Down Expand Up @@ -265,9 +262,15 @@ describe('displays kubernetes plans panel based on availability', () => {
cy.get('[data-qa-plan-row="dedicated-3"]').within(() => {
cy.get('[data-testid="decrement-button"]').should('be.disabled');
cy.get('[data-testid="increment-button"]').should('be.disabled');
cy.findByRole('button', { name: 'Add' }).should('be.disabled');
cy.get('[data-testid="Button"]')
.should(
'have.attr',
'aria-label',
'This plan has limited deployment availability.'
)
.should('be.disabled');
});
cy.findAllByTestId('limited-availability').should('have.length', 2);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 2);
});
});

Expand All @@ -276,7 +279,7 @@ describe('displays kubernetes plans panel based on availability', () => {
// Should contain 3 plans (4 rows including the header row)
// Should have 1 disabled plan
// Should have tooltip for the disabled plan (not more than half disabled plans in the panel)
fbtClick('Shared CPU');
cy.findByText('Shared CPU').click();
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 0);

Expand All @@ -294,7 +297,7 @@ describe('displays kubernetes plans panel based on availability', () => {
'not.have.attr',
'disabled'
);
cy.findAllByTestId('limited-availability').should('have.length', 0);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 0);
});
});

Expand All @@ -303,7 +306,7 @@ describe('displays kubernetes plans panel based on availability', () => {
// Should contain 1 plan (2 rows including the header row)
// Should have one disabled plan
// Should have tooltip for the disabled plan (more than half disabled plans in the panel, but only one plan)
fbtClick('High Memory');
cy.findByText('High Memory').click();
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.limitedAvailability).should('be.visible');
Expand All @@ -314,7 +317,7 @@ describe('displays kubernetes plans panel based on availability', () => {
'have.attr',
'disabled'
);
cy.findAllByTestId('limited-availability').should('have.length', 1);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 1);
});
});

Expand All @@ -324,7 +327,7 @@ describe('displays kubernetes plans panel based on availability', () => {
// Should contain 1 plan (2 rows including the header row)
// Should have its whole panel disabled
// Should not have tooltip for the disabled plan (not needed on disabled panels)
fbtClick('Premium CPU');
cy.findByText('Premium CPU').click();
cy.get(k8PlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.unavailable).should('be.visible');
Expand All @@ -335,7 +338,54 @@ describe('displays kubernetes plans panel based on availability', () => {
'have.attr',
'disabled'
);
cy.findAllByTestId('limited-availability').should('have.length', 0);
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 0);
});
});
});
});

describe('displays specific linode plans for GPU', () => {
before(() => {
mockGetRegions(mockRegions).as('getRegions');
mockGetLinodeTypes(mockLinodeTypes).as('getLinodeTypes');
mockGetRegionAvailability(mockRegions[0].id, mockRegionAvailability).as(
'getRegionAvailability'
);
mockAppendFeatureFlags({
placementGroups: makeFeatureFlagData<Flags['gpuv2']>({
planDivider: true,
}),
});
mockGetFeatureFlagClientstream();
});

it('Should render divided tables when GPU divider enabled', () => {
cy.visitWithLogin('/linodes/create');

ui.regionSelect.find().click();
ui.regionSelect.findItemByRegionLabel(mockRegions[0].label).click();

// GPU tab
// Should display two separate tables
cy.findByText('GPU').click();
cy.get(linodePlansPanel).within(() => {
cy.findAllByRole('alert').should('have.length', 1);
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', {
name: 'List of NVIDIA RTX 4000 Ada Plans',
}).within(() => {
cy.findByText('NVIDIA RTX 4000 Ada').should('be.visible');
cy.findAllByRole('row').should('have.length', 2);
cy.get('[id="gpu-2"]').should('be.disabled');
});

cy.findByRole('table', {
name: 'List of NVIDIA Quadro RTX 6000 Plans',
}).within(() => {
cy.findByText('NVIDIA Quadro RTX 6000').should('be.visible');
cy.findAllByRole('row').should('have.length', 2);
cy.get('[id="gpu-1"]').should('be.disabled');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ export const CardBase = (props: CardBaseProps) => {
} = props;

const renderSubheadings = subheadings.map((subheading, idx) => {
const subHeadingIsString = typeof subheading === 'string';

return (
<CardBaseSubheading
className={subHeadingIsString ? 'cardSubheadingItem' : ''}
data-qa-select-card-subheading={`subheading-${idx + 1}`}
key={idx}
sx={sxSubheading}
Expand All @@ -52,7 +55,10 @@ export const CardBase = (props: CardBaseProps) => {
<CardBaseGrid checked={checked} container spacing={2} sx={sx}>
{renderIcon && <CardBaseIcon sx={sxIcon}>{renderIcon()}</CardBaseIcon>}
<CardBaseHeadings sx={sxHeading}>
<CardBaseHeading data-qa-select-card-heading={heading}>
<CardBaseHeading
className="cardSubheadingTitle"
data-qa-select-card-heading={heading}
>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes help with the styling of the disabled selection card, allowing non string items to not suffer fro the opacity change

{heading}
{headingDecoration}
</CardBaseHeading>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const SelectionCard = React.memo((props: SelectionCardProps) => {
componentsProps={{
tooltip: { sx: sxTooltip },
}}
placement="top-end"
placement="top"
title={tooltip}
>
{cardGrid}
Expand All @@ -196,8 +196,8 @@ const StyledGrid = styled(Grid, {
cursor: 'pointer',
}),
...(props.disabled && {
'& > div': {
opacity: 0.4,
'& .cardSubheadingItem, & .cardSubheadingTitle': {
opacity: 0.3,
},
cursor: 'not-allowed',
}),
Expand Down
4 changes: 2 additions & 2 deletions packages/manager/src/components/TableRow/TableRow.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ export const StyledTableRow = styled(_TableRow, {
...(props.highlight && {
backgroundColor: theme.bg.lightBlue1,
}),
...(props.disabled && {
'&.disabled-row': {
'& td:not(.hasTooltip *), & td:has(.hasTooltip):not(.MuiRadio-root)': {
color:
theme.palette.mode === 'dark' ? theme.color.grey6 : theme.color.grey1,
},
}),
},
}));

export const StyledTableDataCell = styled('td', {
Expand Down
17 changes: 14 additions & 3 deletions packages/manager/src/factories/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import * as Factory from 'factory.ts';

import type { LinodeType } from '@linode/api-v4/lib/linodes/types';
import type { PriceType } from '@linode/api-v4/src/types';
import type { PlanSelectionType } from 'src/features/components/PlansPanel/types';
import type {
PlanSelectionAvailabilityTypes,
PlanWithAvailability,
} from 'src/features/components/PlansPanel/types';
import type { ExtendedType } from 'src/utilities/extendType';

export const typeFactory = Factory.Sync.makeFactory<LinodeType>({
Expand Down Expand Up @@ -54,7 +57,7 @@ export const typeFactory = Factory.Sync.makeFactory<LinodeType>({
vcpus: 8,
});

export const planSelectionTypeFactory = Factory.Sync.makeFactory<PlanSelectionType>(
export const planSelectionTypeFactory = Factory.Sync.makeFactory<PlanWithAvailability>(
{
class: typeFactory.build().class,
disk: typeFactory.build().disk,
Expand All @@ -64,6 +67,9 @@ export const planSelectionTypeFactory = Factory.Sync.makeFactory<PlanSelectionTy
label: typeFactory.build().label,
memory: typeFactory.build().memory,
network_out: typeFactory.build().network_out,
planBelongsToDisabledClass: false,
planHasLimitedAvailability: false,
planIsDisabled512Gb: false,
price: typeFactory.build().price,
region_prices: typeFactory.build().region_prices,
subHeadings: [
Expand All @@ -77,7 +83,9 @@ export const planSelectionTypeFactory = Factory.Sync.makeFactory<PlanSelectionTy
}
);

export const extendedTypeFactory = Factory.Sync.makeFactory<ExtendedType>({
export const extendedTypeFactory = Factory.Sync.makeFactory<
ExtendedType & PlanSelectionAvailabilityTypes
>({
addons: {
backups: {
price: {
Expand Down Expand Up @@ -108,6 +116,9 @@ export const extendedTypeFactory = Factory.Sync.makeFactory<ExtendedType>({
label: typeFactory.build().label,
memory: typeFactory.build().memory,
network_out: typeFactory.build().network_out,
planBelongsToDisabledClass: false,
planHasLimitedAvailability: false,
planIsDisabled512Gb: false,
price: typeFactory.build().price,
region_prices: typeFactory.build().region_prices,
subHeadings: ['$10/mo ($0.015/hr)', '8 CPU, 1024 GB Storage, 16 GB RAM'],
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ interface GeckoFlag {
ga: boolean;
}

interface gpuV2 {
planDivider: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are other properties planned for the feature flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will a string change there will be hence the payload. If not, it's quite ok to have it a JSON flag anyway?

}

type OneClickApp = Record<string, string>;

export interface Flags {
Expand All @@ -61,6 +65,7 @@ export interface Flags {
firewallNodebalancer: boolean;
gecko: boolean; // @TODO gecko: delete this after next release
gecko2: GeckoFlag;
gpuv2: gpuV2;
ipv6Sharing: boolean;
linodeCloneUiChanges: boolean;
linodeCreateRefactor: boolean;
Expand Down
Loading