From dea78334957062d4d3ae34f9bbf3fcc3ac73fa01 Mon Sep 17 00:00:00 2001 From: Christopher Thomas Date: Mon, 13 Apr 2026 14:49:56 +0100 Subject: [PATCH] upcoming: [AISI-22] - Serverless Inference Sidebar Item In Cloud Manager Addition of a new top-level AI feature in navigation to the CM Navbar (PrimaryNav.tsx), aim overall is to create a base for first release of Serverless Inference features within CM, under the AI top level PrimaryNav.tsx : Added AI top level nav, and sub item > Serverless Inference. Also added required NavEntity & ProductFamily entries etc ai.svg: Added provisional AI icon for use in PrimaryNav.tsx ServerlessInference.tsx: Added default Nav location for Serverless Inference. It provides a Landing page for the feature and Tabs to navigate to the other 3 pages. It defaults to routing to Inference Hub InferenceHub.tsx: Added stub for Main landing page for Serverless Inference ModelLibrary.tsx: Added stub for Model Library, where customers can browse AI Models to Inference. ModelPlayground.tsx: Added stub for Model Playground, where customers can try/test models. ApiKeyManagement.tsx: Added stub for Api Key Management, where customers can manage their Inference API Keys Added isServerlessInferenceEnabled feature flag Put Serverless Inference entry in PrimaryNav.tsx behind isServerlessInferenceEnabled flag, feature is hidden by default Put routing to ServerlessInference behind isServerlessInferenceEnabled flag in ServerlessInferenceRoute.tsx. Without access, "Not Found: Page does not exist" notice is shown. --- packages/api-v4/src/account/types.ts | 1 + packages/api-v4/src/cloudpulse/types.ts | 2 + .../src/assets/icons/entityIcons/ai.svg | 5 ++ .../src/components/PrimaryNav/PrimaryNav.tsx | 18 ++++ packages/manager/src/featureFlags.ts | 1 + .../ApiKeyManagement/ApiKeyManagement.tsx | 5 ++ .../InferenceHub/InferenceHub.tsx | 5 ++ .../ModelLibrary/ModelLibrary.tsx | 5 ++ .../ModelPlayground/ModelPlayground.tsx | 5 ++ .../ServerlessInference.tsx | 83 +++++++++++++++++++ .../serverlessInferenceLazyRoute.ts | 9 ++ .../src/features/ServerlessInference/utils.ts | 23 +++++ packages/manager/src/mocks/serverHandlers.ts | 1 + packages/manager/src/routes/index.tsx | 2 + .../ServerlessInferenceRoute.tsx | 21 +++++ .../src/routes/serverlessInference/index.ts | 67 +++++++++++++++ 16 files changed, 253 insertions(+) create mode 100644 packages/manager/src/assets/icons/entityIcons/ai.svg create mode 100644 packages/manager/src/features/ServerlessInference/ApiKeyManagement/ApiKeyManagement.tsx create mode 100644 packages/manager/src/features/ServerlessInference/InferenceHub/InferenceHub.tsx create mode 100644 packages/manager/src/features/ServerlessInference/ModelLibrary/ModelLibrary.tsx create mode 100644 packages/manager/src/features/ServerlessInference/ModelPlayground/ModelPlayground.tsx create mode 100644 packages/manager/src/features/ServerlessInference/ServerlessInference.tsx create mode 100644 packages/manager/src/features/ServerlessInference/serverlessInferenceLazyRoute.ts create mode 100644 packages/manager/src/features/ServerlessInference/utils.ts create mode 100644 packages/manager/src/routes/serverlessInference/ServerlessInferenceRoute.tsx create mode 100644 packages/manager/src/routes/serverlessInference/index.ts diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 22956b38a27..1454cca1ee9 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -60,6 +60,7 @@ export interface Account { export type BillingSource = 'akamai' | 'linode'; export const accountCapabilities = [ + 'AI', 'Akamai Cloud Load Balancer', 'Akamai Cloud Pulse', 'Akamai Cloud Pulse Logs', diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 8a18b38cc0a..f277c36adbf 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -4,6 +4,7 @@ export type AlertSeverityType = 0 | 1 | 2 | 3; export type MetricAggregationType = 'avg' | 'count' | 'max' | 'min' | 'sum'; export type MetricOperatorType = 'eq' | 'gt' | 'gte' | 'lt' | 'lte'; export type CloudPulseServiceType = + | 'ai' | 'blockstorage' | 'dbaas' | 'firewall' @@ -401,6 +402,7 @@ export const capabilityServiceTypeMapping: Record< lke: 'Kubernetes', netloadbalancer: 'Network LoadBalancer', logs: 'Akamai Cloud Pulse Logs', + ai: 'AI', }; /** diff --git a/packages/manager/src/assets/icons/entityIcons/ai.svg b/packages/manager/src/assets/icons/entityIcons/ai.svg new file mode 100644 index 00000000000..2cf5bcbce8c --- /dev/null +++ b/packages/manager/src/assets/icons/entityIcons/ai.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index d9cba488146..4d155b215b5 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -7,6 +7,7 @@ import { Box } from '@linode/ui'; import { useLocation } from '@tanstack/react-router'; import * as React from 'react'; +import AI from 'src/assets/icons/entityIcons/ai.svg'; import Compute from 'src/assets/icons/entityIcons/compute.svg'; import CoreUser from 'src/assets/icons/entityIcons/coreuser.svg'; import Database from 'src/assets/icons/entityIcons/database.svg'; @@ -26,6 +27,7 @@ import { useIsMarketplaceV2Enabled } from 'src/features/Marketplace/shared'; import { useIsNetworkLoadBalancerEnabled } from 'src/features/NetworkLoadBalancers/utils'; import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils'; import { useIsReserveIpEnabled } from 'src/features/ReservedIps/utils'; +import { useIsServerlessInferenceEnabled } from 'src/features/ServerlessInference/utils'; import { useFlags } from 'src/hooks/useFlags'; import PrimaryLink from './PrimaryLink'; @@ -67,6 +69,7 @@ export type NavEntity = | 'Quick Deploy Apps' | 'Quotas' | 'Reserved IPs' + | 'Serverless Inference' | 'Service Transfers' | 'StackScripts' | 'Users & Grants' @@ -75,6 +78,7 @@ export type NavEntity = export type ProductFamily = | 'Administration' + | 'AI' | 'Compute' | 'Databases' | 'Monitor' @@ -141,6 +145,8 @@ export const PrimaryNav = (props: PrimaryNavProps) => { const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); + const { isServerlessInferenceEnabled } = useIsServerlessInferenceEnabled(); + const { isMarketplaceV2FeatureEnabled } = useIsMarketplaceV2Enabled(); const { isReserveIpEnabled } = useIsReserveIpEnabled(); @@ -255,6 +261,17 @@ export const PrimaryNav = (props: PrimaryNavProps) => { ], name: 'Networking', }, + { + icon: , + links: [ + { + display: 'Serverless Inference', + hide: !isServerlessInferenceEnabled, + to: '/serverless-inference', + }, + ], + name: 'AI', + }, { icon: , links: [ @@ -373,6 +390,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { isMarketplaceV2FeatureEnabled, isNetworkLoadBalancerEnabled, isReserveIpEnabled, + isServerlessInferenceEnabled, limitsEvolution, ] ); diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 163615fbf31..0c7897aa862 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -285,6 +285,7 @@ export interface Flags { resourceLock: ResourceLockFlag; secureVmCopy: SecureVMCopy; selfServeBetas: boolean; + serverlessInference: boolean; soldOutChips: boolean; supportTicketSeverity: boolean; taxBanner: TaxBanner; diff --git a/packages/manager/src/features/ServerlessInference/ApiKeyManagement/ApiKeyManagement.tsx b/packages/manager/src/features/ServerlessInference/ApiKeyManagement/ApiKeyManagement.tsx new file mode 100644 index 00000000000..1a690ad60e0 --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/ApiKeyManagement/ApiKeyManagement.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ApiKeyManagement = () => { + return
API Key Management content
; +}; diff --git a/packages/manager/src/features/ServerlessInference/InferenceHub/InferenceHub.tsx b/packages/manager/src/features/ServerlessInference/InferenceHub/InferenceHub.tsx new file mode 100644 index 00000000000..30d05c5fc9f --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/InferenceHub/InferenceHub.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const InferenceHub = () => { + return
Inference Hub content
; +}; diff --git a/packages/manager/src/features/ServerlessInference/ModelLibrary/ModelLibrary.tsx b/packages/manager/src/features/ServerlessInference/ModelLibrary/ModelLibrary.tsx new file mode 100644 index 00000000000..db30078f313 --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/ModelLibrary/ModelLibrary.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ModelLibrary = () => { + return
Model Library content
; +}; diff --git a/packages/manager/src/features/ServerlessInference/ModelPlayground/ModelPlayground.tsx b/packages/manager/src/features/ServerlessInference/ModelPlayground/ModelPlayground.tsx new file mode 100644 index 00000000000..4b58622955e --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/ModelPlayground/ModelPlayground.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const ModelPlayground = () => { + return
Model Playground content
; +}; diff --git a/packages/manager/src/features/ServerlessInference/ServerlessInference.tsx b/packages/manager/src/features/ServerlessInference/ServerlessInference.tsx new file mode 100644 index 00000000000..b7a580b5adb --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/ServerlessInference.tsx @@ -0,0 +1,83 @@ +import { useLocation } from '@tanstack/react-router'; +import * as React from 'react'; + +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { LandingHeader } from 'src/components/LandingHeader'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; +import { TabPanels } from 'src/components/Tabs/TabPanels'; +import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; +import { Tab, useTabs } from 'src/hooks/useTabs'; + +const InferenceHub = React.lazy(() => + import('./InferenceHub/InferenceHub').then((m) => ({ + default: m.InferenceHub, + })) +); + +const ModelPlayground = React.lazy(() => + import('./ModelPlayground/ModelPlayground').then((m) => ({ + default: m.ModelPlayground, + })) +); + +const ApiKeyManagement = React.lazy(() => + import('./ApiKeyManagement/ApiKeyManagement').then((m) => ({ + default: m.ApiKeyManagement, + })) +); + +const ModelLibrary = React.lazy(() => + import('./ModelLibrary/ModelLibrary').then((m) => ({ + default: m.ModelLibrary, + })) +); + +export const ServerlessInference = () => { + // useLocation subscribes to route changes, ensuring the component re-renders + // on navigation so useTabs can recompute the active tab index. + useLocation(); + + const tabs: Tab[] = [ + { title: 'Inference Hub', to: '/serverless-inference/inference-hub' }, + { title: 'Model Playground', to: '/serverless-inference/model-playground' }, + { title: 'Model Library', to: '/serverless-inference/model-library' }, + { + title: 'API Key Management', + to: '/serverless-inference/api-key-management', + }, + ]; + + const { handleTabChange, tabIndex } = useTabs(tabs); + + return ( + + + + + + }> + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/manager/src/features/ServerlessInference/serverlessInferenceLazyRoute.ts b/packages/manager/src/features/ServerlessInference/serverlessInferenceLazyRoute.ts new file mode 100644 index 00000000000..1e727ed9a36 --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/serverlessInferenceLazyRoute.ts @@ -0,0 +1,9 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import { ServerlessInference } from './ServerlessInference'; + +export const serverlessInferenceLazyRoute = createLazyRoute( + '/serverless-inference' +)({ + component: ServerlessInference, +}); diff --git a/packages/manager/src/features/ServerlessInference/utils.ts b/packages/manager/src/features/ServerlessInference/utils.ts new file mode 100644 index 00000000000..1bcaba32490 --- /dev/null +++ b/packages/manager/src/features/ServerlessInference/utils.ts @@ -0,0 +1,23 @@ +import { useAccount } from '@linode/queries'; +import { isFeatureEnabledV2 } from '@linode/utilities'; + +import { useFlags } from 'src/hooks/useFlags'; + +export const useIsServerlessInferenceEnabled = (): { + isServerlessInferenceEnabled: boolean; +} => { + const { data: account } = useAccount(); + const flags = useFlags(); + + if (!flags) { + return { isServerlessInferenceEnabled: false }; + } + + const isServerlessInferenceEnabled = isFeatureEnabledV2( + 'AI', + Boolean(flags.serverlessInference), + account?.capabilities ?? [] + ); + + return { isServerlessInferenceEnabled }; +}; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 3e922548ced..dea4b86386d 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -3958,6 +3958,7 @@ export const handlers = [ http.get('*/monitor/services/:serviceType', ({ params }) => { const serviceType = params.serviceType as CloudPulseServiceType; const serviceTypesMap: Record = { + ai: 'AI', linode: 'Linode', dbaas: 'Databases', nodebalancer: 'NodeBalancers', diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index 2c3ff935a35..07d21d4c410 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -40,6 +40,7 @@ import { quotasRouteTree } from './quotas'; import { reservedIpsRouteTree } from './reservedIps'; import { rootRoute } from './root'; import { searchRouteTree } from './search'; +import { serverlessInferenceRouteTree } from './serverlessInference'; import { serviceTransfersRouteTree } from './serviceTransfers'; import { stackScriptsRouteTree } from './stackscripts'; import { supportRouteTree } from './support'; @@ -91,6 +92,7 @@ export const routeTree = rootRoute.addChildren([ quotasRouteTree, reservedIpsRouteTree, searchRouteTree, + serverlessInferenceRouteTree, serviceTransfersRouteTree, settingsRouteTree, stackScriptsRouteTree, diff --git a/packages/manager/src/routes/serverlessInference/ServerlessInferenceRoute.tsx b/packages/manager/src/routes/serverlessInference/ServerlessInferenceRoute.tsx new file mode 100644 index 00000000000..a8e1493b0f1 --- /dev/null +++ b/packages/manager/src/routes/serverlessInference/ServerlessInferenceRoute.tsx @@ -0,0 +1,21 @@ +import { NotFound } from '@linode/ui'; +import { Outlet } from '@tanstack/react-router'; +import React from 'react'; + +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { useIsServerlessInferenceEnabled } from 'src/features/ServerlessInference/utils'; + +export const ServerlessInferenceRoute = () => { + const { isServerlessInferenceEnabled } = useIsServerlessInferenceEnabled(); + + if (!isServerlessInferenceEnabled) { + return ; + } + return ( + }> + + + + ); +}; diff --git a/packages/manager/src/routes/serverlessInference/index.ts b/packages/manager/src/routes/serverlessInference/index.ts new file mode 100644 index 00000000000..2321c254e51 --- /dev/null +++ b/packages/manager/src/routes/serverlessInference/index.ts @@ -0,0 +1,67 @@ +import { createRoute, redirect } from '@tanstack/react-router'; + +import { rootRoute } from '../root'; +import { ServerlessInferenceRoute } from './ServerlessInferenceRoute'; + +const serverlessInferenceRoute = createRoute({ + component: ServerlessInferenceRoute, + getParentRoute: () => rootRoute, + path: 'serverless-inference', +}); + +const serverlessInferenceIndexRoute = createRoute({ + beforeLoad: async () => { + throw redirect({ to: '/serverless-inference/inference-hub' }); + }, + getParentRoute: () => serverlessInferenceRoute, + path: '/', +}).lazy(() => + import('src/features/ServerlessInference/serverlessInferenceLazyRoute').then( + (m) => m.serverlessInferenceLazyRoute + ) +); + +const serverlessInferenceInferenceHubRoute = createRoute({ + getParentRoute: () => serverlessInferenceRoute, + path: 'inference-hub', +}).lazy(() => + import('src/features/ServerlessInference/serverlessInferenceLazyRoute').then( + (m) => m.serverlessInferenceLazyRoute + ) +); + +const serverlessInferenceModelPlaygroundRoute = createRoute({ + getParentRoute: () => serverlessInferenceRoute, + path: 'model-playground', +}).lazy(() => + import('src/features/ServerlessInference/serverlessInferenceLazyRoute').then( + (m) => m.serverlessInferenceLazyRoute + ) +); + +const serverlessInferenceApiKeyManagementRoute = createRoute({ + getParentRoute: () => serverlessInferenceRoute, + path: 'api-key-management', +}).lazy(() => + import('src/features/ServerlessInference/serverlessInferenceLazyRoute').then( + (m) => m.serverlessInferenceLazyRoute + ) +); + +const serverlessInferenceModelLibraryRoute = createRoute({ + getParentRoute: () => serverlessInferenceRoute, + path: 'model-library', +}).lazy(() => + import('src/features/ServerlessInference/serverlessInferenceLazyRoute').then( + (m) => m.serverlessInferenceLazyRoute + ) +); + +export const serverlessInferenceRouteTree = + serverlessInferenceRoute.addChildren([ + serverlessInferenceIndexRoute, + serverlessInferenceInferenceHubRoute, + serverlessInferenceModelPlaygroundRoute, + serverlessInferenceApiKeyManagementRoute, + serverlessInferenceModelLibraryRoute, + ]);