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": Upcoming Features
---

Disable premium plan tab if corresponding g7 dedicated plans are available ([#13081](https://github.com/linode/manager/pull/13081))
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('clone linode', () => {
beforeEach(() => {
mockAppendFeatureFlags({
linodeInterfaces: { enabled: false },
generationalPlansv2: { enabled: false, allowedPlans: [] },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add a TODO to revisit as this will need to be tested for its real behavior eventually

});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ const mockGPUType = [
}),
];

const mockPremiumType = [
linodeTypeFactory.build({
class: 'premium',
id: 'premium-8',
label: 'Premium 8GB',
}),
];

const mockAcceleratedType = [
linodeTypeFactory.build({
class: 'accelerated',
Expand All @@ -99,6 +107,7 @@ const mockLinodeTypes = [
...mockHighMemoryLinodeTypes,
...mockSharedLinodeTypes,
...mockGPUType,
...mockPremiumType,
...mockAcceleratedType,
];

Expand Down Expand Up @@ -225,7 +234,7 @@ describe('displays linode plans panel based on availability', () => {
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
cy.findAllByRole('row').should('have.length', 3);
cy.get('[id="g7-premium-64"]').should('be.disabled');
cy.findAllByTestId('disabled-plan-tooltip').should('have.length', 0);
});
Expand Down Expand Up @@ -355,7 +364,7 @@ describe('displays kubernetes plans panel based on availability', () => {
cy.get(notices.unavailable).should('be.visible');

cy.findByRole('table', { name: planSelectionTable }).within(() => {
cy.findAllByRole('row').should('have.length', 2);
cy.findAllByRole('row').should('have.length', 3);
cy.get('[data-qa-plan-row="Premium 512 GB"]').should(
'have.attr',
'disabled'
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const options: { flag: keyof Flags; label: string }[] = [
label: 'Firewall Rulesets & Prefixlists',
},
{ flag: 'gecko2', label: 'Gecko' },
{ flag: 'generationalPlans', label: 'Generational compute plans' },
{ flag: 'generationalPlansv2', label: 'Generational compute plans' },
Copy link
Copy Markdown
Contributor

@pmakode-akamai pmakode-akamai Nov 14, 2025

Choose a reason for hiding this comment

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

Since DevTools doesn't support configuring data structures other than booleans and nested booleans, should we remove this from FeatureFlagTool.tsx (here) and rely on setting the flag to true by default for DEV env in LaunchDarkly? Or is it okay to keep the incomplete Generational Plans flag in DevTools for now (it will work for enabled, but allowedPlans can't be configured there)?

May be a good idea to keep it as is at this early stage of development to test locally for now.

@abailly-akamai thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As long as the enabled property is helpful we can leave it here - maybe remove in the next PR?

{ flag: 'limitsEvolution', label: 'Limits Evolution' },
{ flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' },
{ flag: 'linodeInterfaces', label: 'Linode Interfaces' },
Expand Down
6 changes: 5 additions & 1 deletion packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export interface Flags {
disableLargestGbPlans: boolean;
firewallRulesetsPrefixlists: boolean;
gecko2: GeckoFeatureFlag;
generationalPlans: boolean;
generationalPlansv2: GenerationalPlansFlag;
gpuv2: GpuV2;
iam: BetaFeatureFlag;
iamDelegation: BaseFeatureFlag;
Expand Down Expand Up @@ -386,3 +386,7 @@ export type AclpServices = {
metrics?: AclpFlag;
};
};

interface GenerationalPlansFlag extends BaseFeatureFlag {
allowedPlans: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isMTCPlan,
planTabInfoContent,
replaceOrAppendPlaceholder512GbPlans,
useShouldDisablePremiumPlansTab,
} from 'src/features/components/PlansPanel/utils';
import { useFlags } from 'src/hooks/useFlags';

Expand Down Expand Up @@ -82,6 +83,10 @@ export const KubernetesPlansPanel = (props: Props) => {
Boolean(flags.soldOutChips) && Boolean(selectedRegionId)
);

const shouldDisablePremiumPlansTab = useShouldDisablePremiumPlansTab({
types,
});

const isPlanDisabledByAPL = (plan: 'shared' | LinodeTypeClass) =>
plan === 'shared' && Boolean(isAPLEnabled);

Expand Down Expand Up @@ -109,7 +114,7 @@ export const KubernetesPlansPanel = (props: Props) => {
{ isLKE: true }
);

const tabs = Object.keys(plans).map(
const tabs = Object.keys(plans)?.map(
(plan: Exclude<LinodeTypeClass, 'nanode' | 'standard'>) => {
const plansMap: PlanSelectionType[] = plans[plan]!;
const {
Expand All @@ -125,6 +130,7 @@ export const KubernetesPlansPanel = (props: Props) => {
});

return {
disabled: false,
render: () => {
return (
<>
Expand Down Expand Up @@ -170,6 +176,26 @@ export const KubernetesPlansPanel = (props: Props) => {
currentPlanHeading
);

// If there are no premium plans available, plans table will hide the premium tab.
// To override this behavior, we add the tab again and then disable it.
// If there are plans but they should be disabled, we disable the existing tab.
if (
shouldDisablePremiumPlansTab &&
!tabs.some((tab) => tab.title === planTabInfoContent.premium?.title)
) {
tabs.push({
disabled: true,
render: () => <div />,
title: planTabInfoContent.premium?.title,
});
} else if (shouldDisablePremiumPlansTab) {
tabs.forEach((tab) => {
if (tab.title === planTabInfoContent.premium?.title) {
tab.disabled = true;
}
});
}

return (
<TabbedPanel
copy={copy}
Expand All @@ -179,6 +205,11 @@ export const KubernetesPlansPanel = (props: Props) => {
initTab={initialTab >= 0 ? initialTab : 0}
notice={notice}
sx={{ padding: 0 }}
tabDisabledMessage={
shouldDisablePremiumPlansTab
? 'Premium CPUs are now called Dedicated G7 Plans.'
: undefined
}
tabs={tabs}
/>
);
Expand Down
11 changes: 11 additions & 0 deletions packages/manager/src/features/Linodes/LinodeCreate/Plan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useController, useWatch } from 'react-hook-form';

import { DocsLink } from 'src/components/DocsLink/DocsLink';
import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel';
import { useShouldDisablePremiumPlansTab } from 'src/features/components/PlansPanel/utils';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
import { sendLinodeCreateFlowDocsClickEvent } from 'src/utilities/analytics/customEventAnalytics';
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';
Expand All @@ -28,10 +29,15 @@ export const Plan = () => {

const { data: permissions } = usePermissions('account', ['create_linode']);

const shouldDisablePremiumPlansTab = useShouldDisablePremiumPlansTab({
types,
});

return (
<PlansPanel
data-qa-select-plan
disabled={!permissions.create_linode}
disabledTabs={shouldDisablePremiumPlansTab ? ['premium'] : undefined}
docsLink={
<DocsLink
href="https://techdocs.akamai.com/cloud-computing/docs/how-to-choose-a-compute-instance-plan"
Expand All @@ -55,6 +61,11 @@ export const Plan = () => {
selectedId={field.value}
selectedRegionID={regionId}
showLimits
tabDisabledMessage={
shouldDisablePremiumPlansTab
? 'Premium CPUs are now called Dedicated G7 Plans.'
: undefined
}
types={types?.map(extendType) ?? []} // @todo don't extend type
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
planTabInfoContent,
replaceOrAppendPlaceholder512GbPlans,
useIsAcceleratedPlansEnabled,
useShouldDisablePremiumPlansTab,
} from './utils';

import type { PlanSelectionType } from './types';
Expand Down Expand Up @@ -111,6 +112,10 @@ export const PlansPanel = (props: PlansPanelProps) => {
Boolean(flags.soldOutChips) && Boolean(selectedRegionID)
);

const shouldDisablePremiumPlansTab = useShouldDisablePremiumPlansTab({
types,
});

const _types = types.filter((type) => {
if (!isAcceleratedLinodePlansEnabled && type.class === 'accelerated') {
return false;
Expand Down Expand Up @@ -162,7 +167,7 @@ export const PlansPanel = (props: PlansPanelProps) => {

const isDatabaseResize = flow === 'database' && isResize;

const tabs = Object.keys(plans).map(
const tabs = Object.keys(plans)?.map(
(plan: Exclude<LinodeTypeClass, 'nanode' | 'standard'>) => {
const plansMap: PlanSelectionType[] = plans[plan]!;
const {
Expand Down Expand Up @@ -255,6 +260,19 @@ export const PlansPanel = (props: PlansPanelProps) => {
);
}

// If there are no premium plans available, plans table will hide the premium tab.
// To override this behavior, we add the tab again and then disable it.
if (
shouldDisablePremiumPlansTab &&
!tabs.some((tab) => tab.title === planTabInfoContent.premium?.title)
) {
tabs.push({
disabled: true,
render: () => <div />,
title: planTabInfoContent.premium?.title,
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ‘


return (
<TabbedPanel
copy={copy}
Expand Down
18 changes: 18 additions & 0 deletions packages/manager/src/features/components/PlansPanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useAccount } from '@linode/queries';
import { arrayToList, isFeatureEnabledV2 } from '@linode/utilities';

import { useFlags } from 'src/hooks/useFlags';
import { useIsGenerationalPlansEnabled } from 'src/utilities/linodes';

import {
DEDICATED_512_GB_PLAN,
Expand All @@ -23,6 +24,7 @@ import type {
import type {
BaseType,
Capabilities,
LinodeType,
LinodeTypeClass,
Region,
RegionAvailability,
Expand Down Expand Up @@ -495,3 +497,19 @@ export const getDisabledPlanReasonCopy = ({

return PLAN_IS_CURRENTLY_UNAVAILABLE_COPY;
};

export const useShouldDisablePremiumPlansTab = ({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional: Could we also add unit tests for this utility hook (with some allowed plans)? I think It would be good to have coverage for it.

types,
}: {
types: LinodeType[] | PlanSelectionType[] | undefined;
}): boolean => {
const { isGenerationalPlansEnabled, allowedPlans } =
useIsGenerationalPlansEnabled();
// Check if any public premium plans are available.
// We can omit "Premium HT" and "Premium nested" plans as customers don't deploy them using cloud manager.
const arePublicPremiumPlansAvailable = types?.some(
(plan) => plan.class === 'premium' && allowedPlans.includes(plan.id)
);

return Boolean(isGenerationalPlansEnabled) && !arePublicPremiumPlansAvailable;
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
linodeStatsFactory,
linodeTransferFactory,
linodeTypeFactory,
premiumTypeFactory,
premiumHTTypeFactory,
premiumNestedTypeFactory,
} from '@linode/utilities';
import { DateTime } from 'luxon';
import { http } from 'msw';
Expand Down Expand Up @@ -105,7 +106,10 @@ export const getLinodePlans = () => [
const gpuTypesAda = gpuTypeAdaFactory.buildList(20);
const gpuTypesRtx = gpuTypeRtxFactory.buildList(20);
const gpuTypesRtxPro = gpuTypeRtxProFactory.buildList(20);
const premiumTypes = premiumTypeFactory.buildList(6);
const premiumTypes = [
premiumNestedTypeFactory.build(),
premiumHTTypeFactory.build(),
];
const acceleratedType = acceleratedTypeFactory.buildList(7);
const mockPlans = [
nanodeType,
Expand Down
8 changes: 6 additions & 2 deletions packages/manager/src/utilities/linodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ describe('useIsLinodeCloneFirewallEnabled', () => {

describe('useIsGenerationalPlansEnabled', () => {
it('returns isGenerationalPlansEnabled: true if the feature is enabled', () => {
const options = { flags: { generationalPlans: true } };
const options = {
flags: { generationalPlansv2: { enabled: true, allowedPlans: [] } },
};

const { result } = renderHook(() => useIsGenerationalPlansEnabled(), {
wrapper: (ui) => wrapWithTheme(ui, options),
Expand All @@ -92,7 +94,9 @@ describe('useIsGenerationalPlansEnabled', () => {
});

it('returns isGenerationalPlansEnabled: false if the feature is NOT enabled', () => {
const options = { flags: { generationalPlans: false } };
const options = {
flags: { generationalPlansv2: { enabled: false, allowedPlans: [] } },
};

const { result } = renderHook(() => useIsGenerationalPlansEnabled(), {
wrapper: (ui) => wrapWithTheme(ui, options),
Expand Down
3 changes: 2 additions & 1 deletion packages/manager/src/utilities/linodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const useIsGenerationalPlansEnabled = () => {
const flags = useFlags();

return {
isGenerationalPlansEnabled: Boolean(flags.generationalPlans),
isGenerationalPlansEnabled: Boolean(flags.generationalPlansv2?.enabled),
allowedPlans: flags.generationalPlansv2?.allowedPlans || [],
};
};
14 changes: 14 additions & 0 deletions packages/utilities/src/factories/linodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,20 @@ export const gpuTypeRtxProFactory = linodeTypeFactory.extend({

export const premiumTypeFactory = linodeTypeFactory.extend({
class: 'premium',
id: Factory.each((i) => `g7-premium-${i}`),
label: Factory.each((i) => `Premium ${i}GB`),
});

export const premiumNestedTypeFactory = linodeTypeFactory.extend({
class: 'premium',
id: 'g7-premium-112',
label: 'Premium Nested 112GB',
});

export const premiumHTTypeFactory = linodeTypeFactory.extend({
class: 'premium',
id: 'g7-premium-ht-256',
label: 'Premium HT 256GB',
});

export const acceleratedTypeFactory = linodeTypeFactory.extend({
Expand Down