From b9a59da3d290ef9aabf7d09ac51fc51991db6217 Mon Sep 17 00:00:00 2001 From: hrao Date: Tue, 17 Feb 2026 18:37:28 +0530 Subject: [PATCH 01/11] upcoming: [UIE-10232], [UIE-10060] - Add Blackwell GPU related banners and Improve Linode plans display for Dedicated and GPU tabs --- packages/manager/src/featureFlags.ts | 5 ++ .../KubernetesPlanContainer.tsx | 9 +- .../features/Linodes/LinodeCreate/index.tsx | 23 +++++- .../PlansPanel/DedicatedPlanFilters.tsx | 82 +++++++++++++++---- .../components/PlansPanel/GpuFilters.tsx | 82 +++++++++++++++---- .../components/PlansPanel/PlanInformation.tsx | 28 ++++++- .../components/PlansPanel/constants.ts | 1 + .../PlansPanel/types/planFilters.ts | 3 +- .../features/components/PlansPanel/utils.ts | 58 +++++++------ .../PlansPanel/utils/planFilters.ts | 28 ++++++- 10 files changed, 249 insertions(+), 70 deletions(-) diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 38a09b0e616..2a56f99122f 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -248,6 +248,7 @@ export interface Flags { ipv6Sharing: boolean; limitsEvolution: LimitsEvolution; linodeCloneFirewall: boolean; + linodeCreateBanner: LinodeCreateBanner; linodeDiskEncryption: boolean; linodeInterfaces: LinodeInterfacesFlag; lkeEnterprise2: LkeEnterpriseFlag; @@ -435,3 +436,7 @@ export type AclpServices = { interface GenerationalPlansFlag extends BaseFeatureFlag { allowedPlans: string[]; } + +interface LinodeCreateBanner extends BaseFeatureFlag { + message?: string; +} diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx index 94a2edd7b2f..297340816ef 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlanContainer.tsx @@ -5,7 +5,10 @@ import * as React from 'react'; import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; -import { PLAN_PANEL_PAGE_SIZE_OPTIONS } from 'src/features/components/PlansPanel/constants'; +import { + PLAN_FILTER_NO_RESULTS_MESSAGE, + PLAN_PANEL_PAGE_SIZE_OPTIONS, +} from 'src/features/components/PlansPanel/constants'; import { useIsGenerationalPlansEnabled } from 'src/utilities/linodes'; import { PLAN_SELECTION_NO_REGION_SELECTED_MESSAGE } from 'src/utilities/pricing/constants'; @@ -244,7 +247,9 @@ export const KubernetesPlanContainer = ( const plansToDisplay = effectiveFilterResult?.filteredPlans ?? plans; const tableEmptyState = shouldDisplayNoRegionSelectedMessage ? null - : (effectiveFilterResult?.emptyState ?? null); + : plansToDisplay.length === 0 + ? { message: PLAN_FILTER_NO_RESULTS_MESSAGE } + : null; // Feature gate: if pagination is disabled, render the old way if (!isGenerationalPlansEnabled) { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/index.tsx b/packages/manager/src/features/Linodes/LinodeCreate/index.tsx index 6ebf3ca007e..90914838adf 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/index.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/index.tsx @@ -5,7 +5,7 @@ import { useMutateAccountAgreements, useProfile, } from '@linode/queries'; -import { CircleProgress, Notice, Stack } from '@linode/ui'; +import { CircleProgress, Notice, Stack, Typography } from '@linode/ui'; import { scrollErrorIntoView } from '@linode/utilities'; import { useQueryClient } from '@tanstack/react-query'; import { @@ -19,6 +19,7 @@ import React, { useEffect, useRef } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form'; +import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; import { TabPanels } from 'src/components/Tabs/TabPanels'; @@ -45,6 +46,7 @@ import { useIsLinodeCloneFirewallEnabled, useIsLinodeInterfacesEnabled, } from 'src/utilities/linodes'; +import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; import { Actions } from './Actions'; import { AdditionalOptions } from './AdditionalOptions/AdditionalOptions'; @@ -88,7 +90,7 @@ export const LinodeCreate = () => { const { isVMHostMaintenanceEnabled } = useVMHostMaintenanceEnabled(); const linodeCreateType = useGetLinodeCreateType(); - const { aclpServices } = useFlags(); + const { aclpServices, linodeCreateBanner } = useFlags(); // In Create flow, alerts always default to 'legacy' mode const [isAclpAlertsBetaCreateFlow, setIsAclpAlertsBetaCreateFlow] = @@ -246,6 +248,23 @@ export const LinodeCreate = () => { return ( + {linodeCreateBanner?.enabled && ( + + + + )} & { + isDisabled: boolean; +}; + const GENERATION_OPTIONS: SelectOption[] = [ - { label: 'All', value: PLAN_FILTER_ALL }, + { label: 'All Available Plans', value: PLAN_FILTER_ALL_AVAILABLE }, + { label: 'All Plans', value: PLAN_FILTER_ALL }, { label: 'G8 Dedicated', value: PLAN_FILTER_GENERATION_G8 }, { label: 'G7 Dedicated', value: PLAN_FILTER_GENERATION_G7 }, { label: 'G6 Dedicated', value: PLAN_FILTER_GENERATION_G6 }, @@ -61,11 +69,45 @@ const DedicatedPlanFiltersComponent = React.memo( const { disabled = false, onResult, plans, resetPagination } = props; // Local state - persists automatically because component stays mounted - const [generation, setGeneration] = - React.useState(PLAN_FILTER_ALL); + const [generation, setGeneration] = React.useState( + PLAN_FILTER_ALL_AVAILABLE + ); const [type, setType] = React.useState(PLAN_FILTER_ALL); + const generationOptions: GenerationOptionWithDisabled[] = + React.useMemo(() => { + const options = GENERATION_OPTIONS.map((option) => ({ + ...option, + isDisabled: filterPlansByGeneration(plans, option.value).every( + (plan) => getIsPlanDisabled(plan) + ), + })); + const generationRank = { + [PLAN_FILTER_GENERATION_G8]: 3, + [PLAN_FILTER_GENERATION_G7]: 2, + [PLAN_FILTER_GENERATION_G6]: 1, + }; + // Sort options: available first, then all, then by generation (G8 > G7 > G6) + return options.sort((a, b) => { + // "available" always comes first + if (a.value === 'available') return -1; + if (b.value === 'available') return 1; + + // "all" always comes second + if (a.value === 'all') return -1; + if (b.value === 'all') return 1; + + // enabled options before disabled + if (a.isDisabled !== b.isDisabled) { + return Number(a.isDisabled) - Number(b.isDisabled); + } + + // generation order g8 > g7 > g6 + return generationRank[b.value] - generationRank[a.value]; + }); + }, [plans]); + const typeFilteringSupported = supportsTypeFiltering(generation); const typeOptions = typeFilteringSupported @@ -74,7 +116,7 @@ const DedicatedPlanFiltersComponent = React.memo( // Disable type filter if: // 1. Panel is disabled, OR - // 2. Selected generation doesn't support type filtering (G7, G6, All) + // 2. Selected generation doesn't support type filtering (G7, G6, All, All Available) const isTypeSelectDisabled = disabled || !typeFilteringSupported; // Track previous filters to detect changes for pagination reset @@ -102,14 +144,9 @@ const DedicatedPlanFiltersComponent = React.memo( }, [generation, resetPagination, type]); const handleGenerationChange = React.useCallback( - ( - _event: React.SyntheticEvent, - option: null | SelectOption - ) => { - // When clearing, default to "All" instead of undefined - const newGeneration = - (option?.value as PlanFilterGeneration | undefined) ?? - PLAN_FILTER_ALL; + (_event: React.SyntheticEvent, option: GenerationOptionWithDisabled) => { + // When clearing, default to "All Available" instead of undefined + const newGeneration = option?.value ?? PLAN_FILTER_ALL_AVAILABLE; setGeneration(newGeneration); // Reset type filter when generation changes @@ -136,7 +173,9 @@ const DedicatedPlanFiltersComponent = React.memo( }, [generation, plans, type, typeFilteringSupported]); const selectedGenerationOption = React.useMemo(() => { - return GENERATION_OPTIONS.find((opt) => opt.value === generation) ?? null; + return ( + generationOptions.find((opt) => opt.value === generation) ?? undefined + ); }, [generation]); const selectedTypeOption = React.useMemo(() => { @@ -156,15 +195,22 @@ const DedicatedPlanFiltersComponent = React.memo( marginTop: -16, }} > - option.isDisabled || false} id="plan-filter-gpu" label="GPU Plans" onChange={handleGpuTypeChange} @@ -127,7 +173,7 @@ const GPUPlanFilterComponent = React.memo( return { filteredPlans, filterUI, - hasActiveFilters: gpuType !== PLAN_FILTER_ALL, + hasActiveFilters: gpuType !== PLAN_FILTER_ALL_AVAILABLE, }; }, [ GPU_OPTIONS_BASED_ON_AVAILABLE_PLANS, diff --git a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx index f5867e31ec8..9e2589dea6c 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx @@ -11,6 +11,7 @@ import { DEDICATED_COMPUTE_INSTANCES_LINK, GPU_COMPUTE_INSTANCES_LINK, HIGH_MEMORY_COMPUTE_INSTANCES_LINK, + PLAN_FILTER_GPU_RTX_PRO_6000, PREMIUM_COMPUTE_INSTANCES_LINK, SHARED_COMPUTE_INSTANCES_LINK, TRANSFER_COSTS_LINK, @@ -18,9 +19,10 @@ import { import { MetalNotice } from './MetalNotice'; import { PlansAvailabilityNotice } from './PlansAvailabilityNotice'; import { PlanNoticeTypography } from './PlansAvailabilityNotice.styles'; -import { planTabInfoContent } from './utils'; +import { getIsPlanDisabled, planTabInfoContent } from './utils'; +import { filterPlansByGpuType } from './utils/planFilters'; -import type { PlanSelectionType } from './types'; +import type { PlanSelectionType, PlanWithAvailability } from './types'; import type { Region } from '@linode/api-v4'; import type { LinodeTypeClass } from '@linode/api-v4/lib/linodes'; import type { Theme } from '@mui/material/styles'; @@ -66,6 +68,10 @@ export const PlanInformation = (props: PlanInformationProps) => { }; const showGPUEgressBanner = Boolean(useFlags().gpuv2?.egressBanner); const showTransferBanner = Boolean(useFlags().gpuv2?.transferBanner); + const showBlackwellLimitedAvailabilityBanner = filterPlansByGpuType( + plans as PlanWithAvailability[], + PLAN_FILTER_GPU_RTX_PRO_6000 + ).every((plan) => getIsPlanDisabled(plan)); const showLimitedAvailabilityBanner = hasSelectedRegion && @@ -113,6 +119,24 @@ export const PlanInformation = (props: PlanInformationProps) => { planType={planType} regionsData={regionsData || []} /> + {showBlackwellLimitedAvailabilityBanner && ( + + ({ font: theme.font.bold })} + > + + NVIDIA RTX PRO 6000 Blackwell GPU plans are currently + unavailable + {' '} + in this region or globally unavailable. Try another region or{' '} + + contact Support + {' '} + for assistance. + + + )} ) : null} {planType === 'accelerated' && ( diff --git a/packages/manager/src/features/components/PlansPanel/constants.ts b/packages/manager/src/features/components/PlansPanel/constants.ts index b43ae5a3719..04c0505d589 100644 --- a/packages/manager/src/features/components/PlansPanel/constants.ts +++ b/packages/manager/src/features/components/PlansPanel/constants.ts @@ -80,6 +80,7 @@ export const G8_DEDICATED_ALL_SLUGS = [ export const PLAN_FILTER_ALL = 'all'; // Filter option values +export const PLAN_FILTER_ALL_AVAILABLE = 'available'; export const PLAN_FILTER_GENERATION_G8 = 'g8'; export const PLAN_FILTER_GENERATION_G7 = 'g7'; export const PLAN_FILTER_GENERATION_G6 = 'g6'; diff --git a/packages/manager/src/features/components/PlansPanel/types/planFilters.ts b/packages/manager/src/features/components/PlansPanel/types/planFilters.ts index 8e3f37b9eba..440db3dc1b1 100644 --- a/packages/manager/src/features/components/PlansPanel/types/planFilters.ts +++ b/packages/manager/src/features/components/PlansPanel/types/planFilters.ts @@ -14,7 +14,7 @@ import type { PlanWithAvailability } from '../types'; /** * Available plan generations for Dedicated CPU filtering */ -export type PlanFilterGeneration = 'all' | 'g6' | 'g7' | 'g8'; +export type PlanFilterGeneration = 'all' | 'available' | 'g6' | 'g7' | 'g8'; /** * Available plan types for filtering within a generation @@ -26,6 +26,7 @@ export type PlanFilterType = 'all' | 'compute-optimized' | 'general-purpose'; */ export type PlanFilterGPU = | 'all' + | 'available' | 'gpu-rtx4000' | 'gpu-rtx6000' | 'gpu-rtxpro6000'; diff --git a/packages/manager/src/features/components/PlansPanel/utils.ts b/packages/manager/src/features/components/PlansPanel/utils.ts index 6ecc1f5801f..d0955517681 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.ts @@ -408,29 +408,8 @@ export const extractPlansInformation = ({ ); const allDisabledPlans = plansForThisLinodeTypeClass.reduce((acc, plan) => { - const { - planBelongsToDisabledClass, - planHasLimitedAvailability, - planIsDisabled512Gb, - planResizeNotSupported, - planIsSmallerThanUsage, - planIsTooSmall, - planIsTooSmallForAPL, - } = plan; - - // Determine if the plan should be disabled due to - // - belonging to a disabled class - // - having limited availability (API based) - // - being a 512GB plan (hard coded) - if ( - planBelongsToDisabledClass || - planHasLimitedAvailability || - planIsDisabled512Gb || - planResizeNotSupported || - planIsSmallerThanUsage || - planIsTooSmall || - planIsTooSmallForAPL - ) { + const isPlanDisabled = getIsPlanDisabled(plan); + if (isPlanDisabled) { return [...acc, plan]; } @@ -448,6 +427,39 @@ export const extractPlansInformation = ({ }; }; +/** + * + * A utility function to determine if a plan should be disabled based on criteria: + * -belonging to a disabled class + * - having limited availability (API based) + * - being a 512GB plan (hard coded) + * + */ +export const getIsPlanDisabled = (plan: PlanWithAvailability) => { + const { + planBelongsToDisabledClass, + planHasLimitedAvailability, + planIsDisabled512Gb, + planResizeNotSupported, + planIsSmallerThanUsage, + planIsTooSmall, + planIsTooSmallForAPL, + } = plan; + + if ( + planBelongsToDisabledClass || + planHasLimitedAvailability || + planIsDisabled512Gb || + planResizeNotSupported || + planIsSmallerThanUsage || + planIsTooSmall || + planIsTooSmallForAPL + ) { + return true; + } + return false; +}; + /** * A utility function to determine what the disabled plan reason is. * Defaults to the currently unavailable copy. diff --git a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts index 89b8ca003f9..0a17b6e6017 100644 --- a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts +++ b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts @@ -10,9 +10,11 @@ import { G8_DEDICATED_COMPUTE_OPTIMIZED_SLUGS, G8_DEDICATED_GENERAL_PURPOSE_SLUGS, PLAN_FILTER_ALL, + PLAN_FILTER_ALL_AVAILABLE, PLAN_FILTER_TYPE_COMPUTE_OPTIMIZED, PLAN_FILTER_TYPE_GENERAL_PURPOSE, } from '../constants'; +import { getIsPlanDisabled } from '../utils'; import type { PlanFilterGeneration, @@ -51,6 +53,11 @@ export const filterPlansByGeneration = ( return plans; } + //For "Available", return only plans that are not disabled + if (generation === 'available') { + return plans.filter((plan) => !getIsPlanDisabled(plan)); + } + // For G8, use explicit slug list for precise filtering if (generation === 'g8') { const g8Slugs = new Set(G8_DEDICATED_ALL_SLUGS); @@ -108,11 +115,20 @@ export const filterPlansByType = ( generation: PlanFilterGeneration, type: PlanFilterType ): PlanWithAvailability[] => { - // "All" returns all plans, sorted from newest to oldest generations + // "All" returns all plans, sorted based on availability, from newest to oldest generations if (type === PLAN_FILTER_ALL) { - return [...plans].sort( - (a, b) => getGenerationRank(b.id) - getGenerationRank(a.id) - ); + return [...plans].sort((a, b) => { + const isPlanADisabled = getIsPlanDisabled(a); + const isPlanBDisabled = getIsPlanDisabled(b); + + // 1️⃣ Primary sort: Availability (available plans first) + if (isPlanADisabled !== isPlanBDisabled) { + return Number(isPlanADisabled) - Number(isPlanBDisabled); + } + + // 2️⃣ Secondary sort: Generation (newest generation first) + return getGenerationRank(b.id) - getGenerationRank(a.id); + }); } // G7, G6, and "All" generation only have "All" option (no sub-types) @@ -214,6 +230,10 @@ export const filterPlansByGpuType = ( if (!gpuType || gpuType === PLAN_FILTER_ALL) { return plans; } + // For "Available", return only plans that are not disabled + if (gpuType === PLAN_FILTER_ALL_AVAILABLE) { + return plans.filter((plan) => !getIsPlanDisabled(plan)); + } return plans.filter((plan) => plan.id.includes(gpuType)); }; From 2048817b2567bae8e3a15853a04c4ab7e3a80f19 Mon Sep 17 00:00:00 2001 From: hrao Date: Tue, 17 Feb 2026 19:22:19 +0530 Subject: [PATCH 02/11] dont show blackwell availability banner when plans are empty --- .../features/components/PlansPanel/PlanInformation.tsx | 10 ++++++---- .../components/PlansPanel/utils/planFilters.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx index 9e2589dea6c..218bef09483 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx @@ -68,10 +68,12 @@ export const PlanInformation = (props: PlanInformationProps) => { }; const showGPUEgressBanner = Boolean(useFlags().gpuv2?.egressBanner); const showTransferBanner = Boolean(useFlags().gpuv2?.transferBanner); - const showBlackwellLimitedAvailabilityBanner = filterPlansByGpuType( - plans as PlanWithAvailability[], - PLAN_FILTER_GPU_RTX_PRO_6000 - ).every((plan) => getIsPlanDisabled(plan)); + const showBlackwellLimitedAvailabilityBanner = + plans && + filterPlansByGpuType( + plans as PlanWithAvailability[], + PLAN_FILTER_GPU_RTX_PRO_6000 + ).every((plan) => getIsPlanDisabled(plan)); const showLimitedAvailabilityBanner = hasSelectedRegion && diff --git a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts index 0a17b6e6017..fc22a14b9e1 100644 --- a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts +++ b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts @@ -53,7 +53,7 @@ export const filterPlansByGeneration = ( return plans; } - //For "Available", return only plans that are not disabled + // For "Available", return only plans that are not disabled if (generation === 'available') { return plans.filter((plan) => !getIsPlanDisabled(plan)); } From 1c41467c2af0e4bb9073f7fe52a9493051816200 Mon Sep 17 00:00:00 2001 From: hrao Date: Tue, 17 Feb 2026 19:54:58 +0530 Subject: [PATCH 03/11] added sorting for gpu plans table rows based on availability and latest generation --- .../PlansPanel/DedicatedPlanFilters.tsx | 8 ++------ .../components/PlansPanel/GpuFilters.tsx | 18 ++++++----------- .../PlansPanel/utils/planFilters.ts | 20 ++++++++++++++++++- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx b/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx index fbc6b29fcc3..edc85a8309a 100644 --- a/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx +++ b/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx @@ -24,6 +24,7 @@ import { getIsPlanDisabled } from './utils'; import { applyDedicatedPlanFilters, filterPlansByGeneration, + getGenerationRank, supportsTypeFiltering, } from './utils/planFilters'; @@ -83,11 +84,6 @@ const DedicatedPlanFiltersComponent = React.memo( (plan) => getIsPlanDisabled(plan) ), })); - const generationRank = { - [PLAN_FILTER_GENERATION_G8]: 3, - [PLAN_FILTER_GENERATION_G7]: 2, - [PLAN_FILTER_GENERATION_G6]: 1, - }; // Sort options: available first, then all, then by generation (G8 > G7 > G6) return options.sort((a, b) => { // "available" always comes first @@ -104,7 +100,7 @@ const DedicatedPlanFiltersComponent = React.memo( } // generation order g8 > g7 > g6 - return generationRank[b.value] - generationRank[a.value]; + return getGenerationRank(b.value) - getGenerationRank(a.value); }); }, [plans]); diff --git a/packages/manager/src/features/components/PlansPanel/GpuFilters.tsx b/packages/manager/src/features/components/PlansPanel/GpuFilters.tsx index 5826d6261fd..f1f438dec8f 100644 --- a/packages/manager/src/features/components/PlansPanel/GpuFilters.tsx +++ b/packages/manager/src/features/components/PlansPanel/GpuFilters.tsx @@ -16,7 +16,7 @@ import { PLAN_FILTER_GPU_RTX_PRO_6000, } from './constants'; import { getIsPlanDisabled } from './utils'; -import { filterPlansByGpuType } from './utils/planFilters'; +import { filterPlansByGpuType, getGpuRank } from './utils/planFilters'; import type { PlanFilterRenderArgs, @@ -84,11 +84,6 @@ const GPUPlanFilterComponent = React.memo( }, [] ); - const generationRank = { - [PLAN_FILTER_GPU_RTX_PRO_6000]: 3, - [PLAN_FILTER_GPU_RTX_4000_ADA]: 2, - [PLAN_FILTER_GPU_RTX_6000]: 1, - }; // Sort options: available first, then all, then by generation (Blackwell > Ada > Quadro) return options.sort((a, b) => { // "available" always comes first @@ -105,7 +100,7 @@ const GPUPlanFilterComponent = React.memo( } // generation order blackwell > ada > quadro - return generationRank[b.value] - generationRank[a.value]; + return getGpuRank(b.value) - getGpuRank(a.value); }); }, [plans]); @@ -134,9 +129,10 @@ const GPUPlanFilterComponent = React.memo( [] ); - const filteredPlans = React.useMemo(() => { - return filterPlansByGpuType(plans, gpuType); - }, [gpuType, plans]); + const filteredPlans = React.useMemo( + () => filterPlansByGpuType(plans, gpuType), + [gpuType, plans] + ); const selectedGpuType = React.useMemo(() => { return ( @@ -194,8 +190,6 @@ const GPUPlanFilterComponent = React.memo( } ); -GPUPlanFilterComponent.displayName = 'GPUPlanFilterComponent'; - export const createGPUPlanFilterRenderProp = () => { return ({ onResult, diff --git a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts index fc22a14b9e1..d6d3d0c0605 100644 --- a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts +++ b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts @@ -205,6 +205,13 @@ export const applyDedicatedPlanFilters = ( // GPU Filtering // ============================================================================ +export const getGpuRank = (planId: string): number => { + if (planId.includes('rtxpro6000')) return 3; + if (planId.includes('rtx4000')) return 2; + if (planId.includes('rtx6000')) return 1; + return 0; +}; + /** * Filter plans by gpu type * @@ -228,7 +235,18 @@ export const filterPlansByGpuType = ( // For "All", return all plans as-is // The plans array is already filtered to only GPU plans by the parent component if (!gpuType || gpuType === PLAN_FILTER_ALL) { - return plans; + return [...plans].sort((a, b) => { + const isPlanADisabled = getIsPlanDisabled(a); + const isPlanBDisabled = getIsPlanDisabled(b); + + // 1️⃣ Primary sort: Availability (available plans first) + if (isPlanADisabled !== isPlanBDisabled) { + return Number(isPlanADisabled) - Number(isPlanBDisabled); + } + + // 2️⃣ Secondary sort: Generation (newest generation first) + return getGpuRank(b.id) - getGpuRank(a.id); + }); } // For "Available", return only plans that are not disabled if (gpuType === PLAN_FILTER_ALL_AVAILABLE) { From 6f6ddc21d96e9139e74535fdd88914dd20835417 Mon Sep 17 00:00:00 2001 From: hrao Date: Tue, 17 Feb 2026 21:18:36 +0530 Subject: [PATCH 04/11] fix failing test --- .../PlansPanel/utils/planFilters.test.ts | 2 +- .../PlansPanel/utils/planFilters.ts | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/components/PlansPanel/utils/planFilters.test.ts b/packages/manager/src/features/components/PlansPanel/utils/planFilters.test.ts index 4196e192eb0..451da53b221 100644 --- a/packages/manager/src/features/components/PlansPanel/utils/planFilters.test.ts +++ b/packages/manager/src/features/components/PlansPanel/utils/planFilters.test.ts @@ -65,7 +65,7 @@ describe('planFilters utilities', () => { label: 'RTX PRO 6000 Blackwell x1', }); - const gpuPlans = [rtx4000Plan, rtx6000Plan, rtxPro6000Plan]; + const gpuPlans = [rtxPro6000Plan, rtx4000Plan, rtx6000Plan]; describe('filterPlansByGeneration', () => { it('returns only G8 plans that exist in the allow-list', () => { diff --git a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts index d6d3d0c0605..6d72d14f1ea 100644 --- a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts +++ b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts @@ -99,7 +99,7 @@ export const getGenerationRank = (planId: string): number => { * Filter plans by type within a generation * * @param plans - Array of plans (should be pre-filtered by generation) - * @param generation - The generation context ('all', 'g8', 'g7', or 'g6') + * @param generation - The generation context ('all', 'available', 'g8', 'g7', or 'g6') * @param type - The type to filter by ('all', 'compute-optimized', 'general-purpose') * @returns Filtered array of plans matching the type * @@ -205,13 +205,22 @@ export const applyDedicatedPlanFilters = ( // GPU Filtering // ============================================================================ +/** + * Return a numeric rank for a GPU based on plan ID. + * Higher rank = latest gpu(shown first). + * + * Example: + * - "g3-gpu-rtxpro6000-blackwell-1"" -> 3 + * - "g2-gpu-rtx4000a1-s" -> 2 + * - "g1-gpu-rtx6000-1" -> 1 + * - "legacy-plan" -> 0 + */ export const getGpuRank = (planId: string): number => { if (planId.includes('rtxpro6000')) return 3; if (planId.includes('rtx4000')) return 2; if (planId.includes('rtx6000')) return 1; return 0; }; - /** * Filter plans by gpu type * @@ -225,7 +234,7 @@ export const getGpuRank = (planId: string): number => { * // Returns all plans with GPU type 'gpu-rtx4000' * * const allDedicatedPlans = filterPlansByGpuType(allPlans, 'all'); - * // Returns all plans as-is (already filtered by plan type in parent) + * // Returns all plans as-is(already filtered by plan type in parent) sorted based on the latest generation * ``` */ export const filterPlansByGpuType = ( @@ -233,24 +242,29 @@ export const filterPlansByGpuType = ( gpuType?: PlanFilterGPU ): PlanWithAvailability[] => { // For "All", return all plans as-is + // For "available", return only plans that are not disabled, sorted in order of blackwell > ada > quadro // The plans array is already filtered to only GPU plans by the parent component if (!gpuType || gpuType === PLAN_FILTER_ALL) { return [...plans].sort((a, b) => { const isPlanADisabled = getIsPlanDisabled(a); const isPlanBDisabled = getIsPlanDisabled(b); - // 1️⃣ Primary sort: Availability (available plans first) + // Primary sort: Availability (available plans first) if (isPlanADisabled !== isPlanBDisabled) { return Number(isPlanADisabled) - Number(isPlanBDisabled); } - // 2️⃣ Secondary sort: Generation (newest generation first) + // Secondary sort: Generation (newest generation first) return getGpuRank(b.id) - getGpuRank(a.id); }); } // For "Available", return only plans that are not disabled if (gpuType === PLAN_FILTER_ALL_AVAILABLE) { - return plans.filter((plan) => !getIsPlanDisabled(plan)); + return plans + .filter((plan) => !getIsPlanDisabled(plan)) + .sort((a, b) => { + return getGenerationRank(b.id) - getGenerationRank(a.id); + }); } return plans.filter((plan) => plan.id.includes(gpuType)); }; From 61295f906de45ebc5c9270ed4c582bdbfc8cf722 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 17 Feb 2026 14:13:17 -0500 Subject: [PATCH 05/11] Fix failing plan selection tests --- .../e2e/core/linodes/plan-selection.spec.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) 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 bd92c821f9c..a0eabad7c27 100644 --- a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts @@ -143,6 +143,13 @@ const notices = { unavailable: '[data-qa-error="true"]', }; +const GPU_GENERAL_AVAILABILITY_NOTICE = + 'New GPU instances are now generally available. Deploy an RTX 4000 Ada GPU instance in select core compute regions in North America, Europe, and Asia.'; +const GPU_NO_AVAILABILITY_ERROR = + 'GPU Plans are not currently available in this region.'; +const GPU_BLACKWELL_NO_AVAILABILITY_ERROR = + 'NVIDIA RTX PRO 6000 Blackwell GPU plans are currently unavailable in this region or globally unavailable. Try another region or contact Support for assistance.'; + authenticate(); describe('displays linode plans panel based on availability', () => { beforeEach(() => { @@ -399,10 +406,16 @@ describe('displays specific linode plans for GPU', () => { .click(); // GPU tab - // Should display two separate tables + // Confirm that the expected notice/error banners are present: + // + // - General availability notice explaining that Nvidia Ada plans are available. + // - Region availability error explaining that GPU plans are unavailable in the mocked region. + // - Blackwell GPU availability error explaining that Blackwell plans are unavailable. cy.findByText('GPU').click(); cy.get(linodePlansPanel).within(() => { - cy.findAllByRole('alert').should('have.length', 3); + cy.contains(GPU_GENERAL_AVAILABILITY_NOTICE).should('be.visible'); + cy.contains(GPU_NO_AVAILABILITY_ERROR).should('be.visible'); + cy.contains(GPU_BLACKWELL_NO_AVAILABILITY_ERROR).should('be.visible'); cy.get(notices.unavailable).should('be.visible'); cy.findByRole('table', { @@ -440,11 +453,16 @@ describe('displays specific kubernetes plans for GPU', () => { .click(); // GPU tab - // Should display two separate tables + // Confirm that the expected notice/error banners are present: + // + // - General availability notice explaining that Nvidia Ada plans are available. + // - Region availability error explaining that GPU plans are unavailable in the mocked region. + // - Blackwell GPU availability error explaining that Blackwell plans are unavailable. cy.findByText('GPU').click(); cy.get(k8PlansPanel).within(() => { - cy.findAllByRole('alert').should('have.length', 3); - cy.get(notices.unavailable).should('be.visible'); + cy.contains(GPU_GENERAL_AVAILABILITY_NOTICE).should('be.visible'); + cy.contains(GPU_NO_AVAILABILITY_ERROR).should('be.visible'); + cy.contains(GPU_BLACKWELL_NO_AVAILABILITY_ERROR).should('be.visible'); cy.findByRole('table', { name: 'List of Linode Plans', From dc1f982eaf74efa4b77455e267f7fda127082d5f Mon Sep 17 00:00:00 2001 From: hrao Date: Wed, 18 Feb 2026 11:47:04 +0530 Subject: [PATCH 06/11] PR feedback @dwiley-akamai --- .../manager/src/features/Linodes/LinodeCreate/index.tsx | 2 +- .../features/components/PlansPanel/DedicatedPlanFilters.tsx | 2 +- .../manager/src/features/components/PlansPanel/utils.ts | 2 +- .../src/features/components/PlansPanel/utils/planFilters.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/index.tsx b/packages/manager/src/features/Linodes/LinodeCreate/index.tsx index 90914838adf..9316204ef9e 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/index.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/index.tsx @@ -250,7 +250,7 @@ export const LinodeCreate = () => { {linodeCreateBanner?.enabled && ( diff --git a/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx b/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx index edc85a8309a..815ed8aee30 100644 --- a/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx +++ b/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx @@ -141,7 +141,7 @@ const DedicatedPlanFiltersComponent = React.memo( const handleGenerationChange = React.useCallback( (_event: React.SyntheticEvent, option: GenerationOptionWithDisabled) => { - // When clearing, default to "All Available" instead of undefined + // if option is undefined, default to "All Available" instead const newGeneration = option?.value ?? PLAN_FILTER_ALL_AVAILABLE; setGeneration(newGeneration); diff --git a/packages/manager/src/features/components/PlansPanel/utils.ts b/packages/manager/src/features/components/PlansPanel/utils.ts index d0955517681..981e589a8b3 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.ts @@ -430,7 +430,7 @@ export const extractPlansInformation = ({ /** * * A utility function to determine if a plan should be disabled based on criteria: - * -belonging to a disabled class + * - belonging to a disabled class * - having limited availability (API based) * - being a 512GB plan (hard coded) * diff --git a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts index 6d72d14f1ea..55decf2f008 100644 --- a/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts +++ b/packages/manager/src/features/components/PlansPanel/utils/planFilters.ts @@ -121,12 +121,12 @@ export const filterPlansByType = ( const isPlanADisabled = getIsPlanDisabled(a); const isPlanBDisabled = getIsPlanDisabled(b); - // 1️⃣ Primary sort: Availability (available plans first) + // Primary sort: Availability (available plans first) if (isPlanADisabled !== isPlanBDisabled) { return Number(isPlanADisabled) - Number(isPlanBDisabled); } - // 2️⃣ Secondary sort: Generation (newest generation first) + // Secondary sort: Generation (newest generation first) return getGenerationRank(b.id) - getGenerationRank(a.id); }); } @@ -234,7 +234,7 @@ export const getGpuRank = (planId: string): number => { * // Returns all plans with GPU type 'gpu-rtx4000' * * const allDedicatedPlans = filterPlansByGpuType(allPlans, 'all'); - * // Returns all plans as-is(already filtered by plan type in parent) sorted based on the latest generation + * // Returns all plans as-is (already filtered by plan type in parent) sorted based on the latest generation * ``` */ export const filterPlansByGpuType = ( From ba25f31d0c99b00d9986236fcf7cac17436a47b7 Mon Sep 17 00:00:00 2001 From: hrao Date: Wed, 18 Feb 2026 11:48:52 +0530 Subject: [PATCH 07/11] Added changeset: Improve Linode plans' display for Dedicated and GPU tabs --- .../manager/.changeset/pr-13408-changed-1771395532024.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13408-changed-1771395532024.md diff --git a/packages/manager/.changeset/pr-13408-changed-1771395532024.md b/packages/manager/.changeset/pr-13408-changed-1771395532024.md new file mode 100644 index 00000000000..98f2f9a8490 --- /dev/null +++ b/packages/manager/.changeset/pr-13408-changed-1771395532024.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Improve Linode plans' display for Dedicated and GPU tabs ([#13408](https://github.com/linode/manager/pull/13408)) From 0e7f09a33102bf421bedea6a8531b890a34069cc Mon Sep 17 00:00:00 2001 From: hrao Date: Wed, 18 Feb 2026 11:50:10 +0530 Subject: [PATCH 08/11] Added changeset: Add Blackwell GPU related banners in the Linode Create page --- .../.changeset/pr-13408-upcoming-features-1771395610474.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13408-upcoming-features-1771395610474.md diff --git a/packages/manager/.changeset/pr-13408-upcoming-features-1771395610474.md b/packages/manager/.changeset/pr-13408-upcoming-features-1771395610474.md new file mode 100644 index 00000000000..898c74e0b6b --- /dev/null +++ b/packages/manager/.changeset/pr-13408-upcoming-features-1771395610474.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Blackwell GPU related banners in the Linode Create page ([#13408](https://github.com/linode/manager/pull/13408)) From d47f7825378f93994f32a33c36c476a9340baf13 Mon Sep 17 00:00:00 2001 From: hrao Date: Wed, 18 Feb 2026 19:23:24 +0530 Subject: [PATCH 09/11] PR feedback @tvijay-akamai --- .../features/components/PlansPanel/DedicatedPlanFilters.tsx | 2 +- .../src/features/components/PlansPanel/PlanInformation.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx b/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx index 815ed8aee30..e07f4219a34 100644 --- a/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx +++ b/packages/manager/src/features/components/PlansPanel/DedicatedPlanFilters.tsx @@ -172,7 +172,7 @@ const DedicatedPlanFiltersComponent = React.memo( return ( generationOptions.find((opt) => opt.value === generation) ?? undefined ); - }, [generation]); + }, [generation, generationOptions]); const selectedTypeOption = React.useMemo(() => { const displayType = typeFilteringSupported ? type : PLAN_FILTER_ALL; diff --git a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx index 218bef09483..cdfc62406f6 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx @@ -68,8 +68,10 @@ export const PlanInformation = (props: PlanInformationProps) => { }; const showGPUEgressBanner = Boolean(useFlags().gpuv2?.egressBanner); const showTransferBanner = Boolean(useFlags().gpuv2?.transferBanner); + const showBlackwellLimitedAvailabilityBanner = - plans && + hasSelectedRegion && + plans?.length && filterPlansByGpuType( plans as PlanWithAvailability[], PLAN_FILTER_GPU_RTX_PRO_6000 From 911af80a3fa894895e03e2df728ad8a5a7407979 Mon Sep 17 00:00:00 2001 From: hrao Date: Wed, 18 Feb 2026 19:46:29 +0530 Subject: [PATCH 10/11] added support for deriving pendo id from LD flag --- packages/manager/src/featureFlags.ts | 1 + packages/manager/src/features/Linodes/LinodeCreate/index.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 2a56f99122f..3862ee35e25 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -439,4 +439,5 @@ interface GenerationalPlansFlag extends BaseFeatureFlag { interface LinodeCreateBanner extends BaseFeatureFlag { message?: string; + pendo_id?: string; } diff --git a/packages/manager/src/features/Linodes/LinodeCreate/index.tsx b/packages/manager/src/features/Linodes/LinodeCreate/index.tsx index 9316204ef9e..8743bd46ac4 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/index.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/index.tsx @@ -253,6 +253,9 @@ export const LinodeCreate = () => { preferenceKey="linode-create-banner" spacingBottom={8} variant="info" + {...(linodeCreateBanner?.enabled && { + 'data-pendo-id': linodeCreateBanner?.pendo_id, + })} > Date: Wed, 18 Feb 2026 20:09:51 +0530 Subject: [PATCH 11/11] made the getIsPlanDisabled check more shorter --- .../manager/src/features/components/PlansPanel/utils.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/components/PlansPanel/utils.ts b/packages/manager/src/features/components/PlansPanel/utils.ts index 981e589a8b3..56f6108786e 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.ts @@ -446,7 +446,7 @@ export const getIsPlanDisabled = (plan: PlanWithAvailability) => { planIsTooSmallForAPL, } = plan; - if ( + return ( planBelongsToDisabledClass || planHasLimitedAvailability || planIsDisabled512Gb || @@ -454,10 +454,7 @@ export const getIsPlanDisabled = (plan: PlanWithAvailability) => { planIsSmallerThanUsage || planIsTooSmall || planIsTooSmallForAPL - ) { - return true; - } - return false; + ); }; /**