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 93ed6a9b6cc..3a367e41ef5 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' @@ -140,6 +144,8 @@ export const PrimaryNav = (props: PrimaryNavProps) => { const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); + const { isServerlessInferenceEnabled } = useIsServerlessInferenceEnabled(); + const { isMarketplaceV2FeatureEnabled } = useIsMarketplaceV2Enabled(); const { isReserveIpEnabled } = useIsReserveIpEnabled(); @@ -254,6 +260,17 @@ export const PrimaryNav = (props: PrimaryNavProps) => { ], name: 'Networking', }, + { + icon: , + links: [ + { + display: 'Serverless Inference', + hide: !isServerlessInferenceEnabled, + to: '/serverless-inference', + }, + ], + name: 'AI', + }, { icon: , links: [ @@ -372,6 +389,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 02e26be0afc..98e6b3b87e8 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -291,6 +291,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, + ]);