Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3cb76f7
Capture netbird.ai signup source before authentication
mlsmaycon Jul 12, 2026
b62a184
Resolve Agent Network mode from account settings
mlsmaycon Jul 12, 2026
b0b9c38
Enable Agent Network focused view for netbird.ai signups
mlsmaycon Jul 12, 2026
d4217db
Add Agent Network focused view toggle to client settings
mlsmaycon Jul 12, 2026
7dd95af
Swap remaining agent network gating call sites
mlsmaycon Jul 12, 2026
fb409b7
Use signup_source query parameter for signup source capture
mlsmaycon Jul 12, 2026
b280848
Resolve Agent Network mode optimistically for pending netbird.ai signups
mlsmaycon Jul 12, 2026
d16672f
Respect explicit agent_network_only opt-out over deployment config
mlsmaycon Jul 12, 2026
55f2847
Merge remote-tracking branch 'origin/main' into feature/agent-network…
mlsmaycon Jul 12, 2026
cc7b811
Add e2e spec for Agent Network focused view menu and routes
mlsmaycon Jul 12, 2026
8920b2d
Anchor focused-view e2e assertions on always-present nav to avoid bac…
mlsmaycon Jul 12, 2026
38dc2aa
Deflake focused-view e2e: assert route reachability instead of sideba…
mlsmaycon Jul 12, 2026
2aa59ca
Fix onboarding form closing when Agent Network focused view is applied
mlsmaycon Jul 12, 2026
7ad5be9
Show the agent-network signup form on cloud for netbird.ai signups
mlsmaycon Jul 12, 2026
dd35a33
Defer onboarding until Agent Network mode resolves to avoid wrong-for…
mlsmaycon Jul 12, 2026
0fff98c
Decide onboarding form from signup_source so the regular form never f…
mlsmaycon Jul 12, 2026
6c0a29b
Add e2e coverage for onboarding form selection
mlsmaycon Jul 12, 2026
0ef296f
Let loginToApp expect an onboarding overlay instead of dismissing the…
mlsmaycon Jul 12, 2026
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
21 changes: 14 additions & 7 deletions src/app/(dashboard)/agent-network/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
"use client";

import { isAgentNetworkEnabled } from "@utils/netbird";
import { notFound } from "next/navigation";
import * as React from "react";
import { useAgentNetworkMode } from "@/modules/agent-network/useAgentNetworkMode";

// Gates the entire Agent Network route tree behind the NETBIRD_AGENT_NETWORK
// flag. When disabled, these routes don't exist as far as the user is
// concerned — the dashboard behaves exactly as it did without the feature.
// flag or the account-level agent_network_only setting. When disabled, these
// routes don't exist as far as the user is concerned — the dashboard behaves
// exactly as it did without the feature. Rendering waits for the account to
// load so accounts enabled via settings don't get a redirect flash.
export default function AgentNetworkLayout({
children,
}: {
children: React.ReactNode;
}) {
if (!isAgentNetworkEnabled()) {
notFound();
const { enabled, loading } = useAgentNetworkMode();

if (enabled) {
return <>{children}</>;
}
if (loading) {
return null;
}
return <>{children}</>;
}
notFound();
}
12 changes: 7 additions & 5 deletions src/app/(dashboard)/control-center/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import SquareIcon from "@components/SquareIcon";
import GetStartedTest from "@components/ui/GetStartedTest";
import { SmallBadge } from "@components/ui/SmallBadge";
import useFetchApi from "@utils/api";
import { isAgentNetworkEnabled, isAgentNetworkOnly } from "@utils/netbird";
import {
Background,
Edge,
Expand Down Expand Up @@ -54,6 +53,7 @@ import AIProvidersProvider, {
useAIProviders,
} from "@/modules/agent-network/AIProvidersProvider";
import { AIProviderId } from "@/modules/agent-network/data/mockData";
import { useAgentNetworkMode } from "@/modules/agent-network/useAgentNetworkMode";
import { FlowSelector, FlowView } from "@/modules/control-center/FlowSelector";
import { NetworkRoutingPeerCount } from "@/modules/control-center/NetworkRoutingPeerCount";
import { ControlCenterCurrentUserBadge } from "@/modules/control-center/user/ControlCenterCurrentUserBadge";
Expand Down Expand Up @@ -90,6 +90,8 @@ function ControlCenterView() {
const [layoutInitialized, setLayoutInitialized] = useState(false);
const [forceLayoutChange, setForceLayoutChange] = useState(false);
const { loggedInUser } = useLoggedInUser();
const { only: agentNetworkOnly, enabled: agentNetworkEnabled } =
useAgentNetworkMode();

const queryParams = useSearchParams();
const queryTab = queryParams.get("tab");
Expand Down Expand Up @@ -125,13 +127,13 @@ function ControlCenterView() {
"/agent-network/providers",
true,
true,
isAgentNetworkEnabled(),
agentNetworkEnabled,
);
const { data: agentPolicies } = useFetchApi<APIPolicy[]>(
"/agent-network/policies",
true,
true,
isAgentNetworkEnabled(),
agentNetworkEnabled,
);

// providerById lets the overlay look up a Provider's display payload
Expand Down Expand Up @@ -1995,7 +1997,7 @@ function ControlCenterView() {
{/* Networks is dropped as a top-level pivot in the
agent-network repackaging — keep the dropdown + per-network
chrome for everyone else so flag-off behaviour is unchanged. */}
{!isAgentNetworkOnly() && currentView === "networks" && (
{!agentNetworkOnly && currentView === "networks" && (
<div className={"w-64"}>
<SelectDropdown
variant={"secondary"}
Expand All @@ -2011,7 +2013,7 @@ function ControlCenterView() {
</div>
)}

{!isAgentNetworkOnly() && selectedNetwork && currentNetwork && (
{!agentNetworkOnly && selectedNetwork && currentNetwork && (
<NetworkRoutingPeerCount network={currentNetwork} />
)}
</div>
Expand Down
48 changes: 47 additions & 1 deletion src/cloud/contexts/NetBirdCloudProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { notify } from "@components/Notification";
import { useApiCall } from "@utils/api";
import loadConfig from "@utils/config";
import { isNetBirdCloud } from "@utils/netbird";
import * as React from "react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useSWRConfig } from "swr";
import { Hubspot, submitHubspotForm } from "@/cloud/analytics/Hubspot";
import { AWSChoosePlan } from "@/cloud/aws/AWSChoosePlan";
Expand All @@ -11,8 +12,14 @@ import { useDomainCategory } from "@/cloud/cloud-hooks/useDomainCategory";
import HowDidYouHearAboutUs from "@/cloud/survey/HowDidYouHearAboutUs";
import { useAnalytics } from "@/contexts/AnalyticsProvider";
import { useBilling } from "@/contexts/BillingProvider";
import {
AGENT_NETWORK_SIGNUP_SOURCE,
SIGNUP_SOURCE_LOCAL_STORAGE_KEY,
} from "@/hooks/useSignupSource";
import type { Account } from "@/interfaces/Account";
import type { Group } from "@/interfaces/Group";
import { PlanTier } from "@/interfaces/Subscription";
import { useAccount } from "@/modules/account/useAccount";
import { OnboardingProvider } from "@/modules/onboarding/OnboardingProvider";

export const NetBirdCloudProvider = () => {
Expand All @@ -25,6 +32,9 @@ export const NetBirdCloudProvider = () => {
"/integrations/billing/aws/marketplace/enrich",
true,
).post;
const account = useAccount();
const accountRequest = useApiCall<Account>("/accounts", true).put;
const signupSourceApplied = useRef(false);

useEffect(() => {
try {
Expand All @@ -41,6 +51,42 @@ export const NetBirdCloudProvider = () => {
} catch (e) {}
}, []);

// Apply the netbird.ai signup source once the account is available. Only
// new accounts (signup form still pending) are switched to the Agent
// Network focused view — a stale flag from an existing user is discarded.
useEffect(() => {
if (!account || signupSourceApplied.current) return;
try {
const source = localStorage.getItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY);
if (source !== AGENT_NETWORK_SIGNUP_SOURCE) return;

if (
account.onboarding?.signup_form_pending !== true ||
account.settings?.agent_network_only === true
) {
localStorage.removeItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY);
return;
}

signupSourceApplied.current = true;
notify({
title: "Agent Network",
description: "Agent Network focused view enabled for your account.",
promise: accountRequest(
{
id: account.id,
settings: { ...account.settings, agent_network_only: true },
},
"/" + account.id,
).then(() => {
mutate("/accounts");
localStorage.removeItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY);
}),
loadingMessage: "Enabling Agent Network focused view...",
});
} catch (e) {}
}, [account]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const hasFreeOrTrialAWSPlan =
subscription?.plan_tier === PlanTier.FREE ||
subscription?.plan_tier === PlanTier.TRIAL;
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/useSignupSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect } from "react";

export const SIGNUP_SOURCE_QUERY_PARAM = "signup_source";
export const SIGNUP_SOURCE_LOCAL_STORAGE_KEY = "netbird-signup-source";
export const AGENT_NETWORK_SIGNUP_SOURCE = "netbird.ai";

/**
* Store the signup_source query parameter into localStorage so it survives
* the OIDC redirect and can be applied once the account is available.
*/
export function useSignupSource() {
useEffect(() => {
if (typeof window === "undefined") return;

const params = new URLSearchParams(window.location.search);
const source = params.get(SIGNUP_SOURCE_QUERY_PARAM);

if (source === AGENT_NETWORK_SIGNUP_SOURCE) {
try {
localStorage.setItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY, source);
} catch (e) {}
}
}, []);
}
1 change: 1 addition & 0 deletions src/interfaces/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface Account {
local_mfa_enabled?: boolean;
ipv6_enabled_groups?: string[];
network_range_v6?: string;
agent_network_only?: boolean;
};
onboarding?: AccountOnboarding;
}
Expand Down
2 changes: 2 additions & 0 deletions src/layouts/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ErrorBoundaryProvider from "@/contexts/ErrorBoundary";
import { GlobalThemeProvider } from "@/contexts/GlobalThemeProvider";
import InstanceSetupProvider from "@/contexts/InstanceSetupProvider";
import { NavigationEvents } from "@/contexts/NavigationEvents";
import { useSignupSource } from "@/hooks/useSignupSource";

const inter = localFont({
src: "../assets/fonts/Inter.ttf",
Expand All @@ -39,6 +40,7 @@ export default function AppLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
useAWSMarketplace();
useSignupSource();

return (
<html lang="en">
Expand Down
40 changes: 16 additions & 24 deletions src/layouts/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

import { ScrollArea } from "@components/ScrollArea";
import { cn } from "@utils/helpers";
import {
isAgentNetworkEnabled,
isAgentNetworkOnly,
isNetBirdCloud,
} from "@utils/netbird";
import { isNetBirdCloud } from "@utils/netbird";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import AgentNetworkIcon from "@/assets/icons/AgentNetworkIcon";
import ControlCenterIcon from "@/assets/icons/ControlCenterIcon";
Expand All @@ -24,6 +20,7 @@ import { useAnnouncement } from "@/contexts/AnnouncementProvider";
import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { headerHeight } from "@/layouts/Header";
import { useAgentNetworkMode } from "@/modules/agent-network/useAgentNetworkMode";
import { NavigationUsageInfo } from "@/modules/billing/NavigationUsageInfo";
import { NetworkNavigation } from "@/modules/networks/misc/NetworkNavigation";
import { SmallBadge } from "@components/ui/SmallBadge";
Expand All @@ -43,6 +40,8 @@ export default function Navigation({
const { bannerHeight } = useAnnouncement();
const { isNavigationCollapsed } = useApplicationContext();
const { permission, isRestricted } = usePermissions();
const { only: agentNetworkOnly, enabled: agentNetworkEnabled } =
useAgentNetworkMode();

return (
<div
Expand Down Expand Up @@ -126,7 +125,7 @@ export default function Navigation({
/>
</SidebarItem>

{!isAgentNetworkOnly() && <NetworkNavigation />}
{!agentNetworkOnly && <NetworkNavigation />}

<SidebarItem
icon={<ReverseProxyIcon size={16} />}
Expand All @@ -145,7 +144,7 @@ export default function Navigation({
href={"/reverse-proxy"}
collapsible
exactPathMatch={false}
visible={permission?.services?.read && !isAgentNetworkOnly()}
visible={permission?.services?.read && !agentNetworkOnly}
>
<SidebarItem
label="Services"
Expand Down Expand Up @@ -183,7 +182,7 @@ export default function Navigation({
label={
<div className={"flex items-center gap-2"}>
Agent Network
{!isAgentNetworkOnly() && (
{!agentNetworkOnly && (
<SmallBadge
text={"Beta"}
variant={"sky"}
Expand All @@ -200,44 +199,36 @@ export default function Navigation({
exactPathMatch={false}
// Parent is visible when at least one child is permitted. All
// Agent Network pages guard on services.read, so the section
// tracks that (plus the feature flag).
visible={isAgentNetworkEnabled() && permission?.services?.read}
// tracks that (plus the feature gating).
visible={agentNetworkEnabled && permission?.services?.read}
>
<SidebarItem
label="Providers"
isChild
href={"/agent-network/providers"}
exactPathMatch={true}
visible={
isAgentNetworkEnabled() && permission?.services?.read
}
visible={agentNetworkEnabled && permission?.services?.read}
/>
<SidebarItem
label="Policies"
isChild
href={"/agent-network/policies"}
exactPathMatch={true}
visible={
isAgentNetworkEnabled() && permission?.services?.read
}
visible={agentNetworkEnabled && permission?.services?.read}
/>
<SidebarItem
label="Usage & Logs"
isChild
href={"/agent-network/usage"}
exactPathMatch={true}
visible={
isAgentNetworkEnabled() && permission?.services?.read
}
visible={agentNetworkEnabled && permission?.services?.read}
/>
<SidebarItem
label="Configuration"
isChild
href={"/agent-network/configuration"}
exactPathMatch={true}
visible={
isAgentNetworkEnabled() && permission?.services?.read
}
visible={agentNetworkEnabled && permission?.services?.read}
/>
</SidebarItem>

Expand All @@ -249,7 +240,7 @@ export default function Navigation({
exactPathMatch={true}
visible={
(permission.dns.read || permission.nameservers.read) &&
!isAgentNetworkOnly()
!agentNetworkOnly
}
>
<SidebarItem
Expand Down Expand Up @@ -351,14 +342,15 @@ export function SidebarItemGroup({ children }: SidebarItemGroupProps) {

const ActivityNavigationItem = () => {
const { permission } = usePermissions();
const { only: agentNetworkOnly } = useAgentNetworkMode();

return (
<SidebarItem
icon={<ActivityIcon />}
label="Activity"
href={"/events"}
collapsible
visible={permission.events.read && !isAgentNetworkOnly()}
visible={permission.events.read && !agentNetworkOnly}
>
<SidebarItem
label="Audit Events"
Expand Down
7 changes: 4 additions & 3 deletions src/modules/agent-network/AIProvidersProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { notify } from "@components/Notification";
import useFetchApi, { useApiCall } from "@utils/api";
import { isAgentNetworkEnabled } from "@utils/netbird";
import React, {
createContext,
useCallback,
Expand All @@ -22,6 +21,7 @@ import {
PolicyLimits,
ProviderModel,
} from "@/modules/agent-network/data/mockData";
import { useAgentNetworkMode } from "@/modules/agent-network/useAgentNetworkMode";

export type APIProviderModel = {
id: string;
Expand Down Expand Up @@ -501,12 +501,13 @@ export function useAIProviders() {
// which we still tolerate via ignoreError so older deploys don't surface
// a spurious error in the empty state.
export function useAgentNetworkSettings() {
const { enabled: agentNetworkEnabled } = useAgentNetworkMode();
const { data, error, isLoading, mutate } =
useFetchApi<APIAgentNetworkSettings>(
"/agent-network/settings",
true,
true,
isAgentNetworkEnabled(),
agentNetworkEnabled,
);
const settings = useMemo<AgentNetworkSettings | null>(
() => (data ? settingsFromAPI(data) : null),
Expand All @@ -528,7 +529,7 @@ export default function AIProvidersProvider({ children }: Readonly<Props>) {
// disabled — it can safely wrap surfaces like the Control Center
// without hitting agent-network endpoints in deployments that don't
// have the feature.
const agentNetworkEnabled = isAgentNetworkEnabled();
const { enabled: agentNetworkEnabled } = useAgentNetworkMode();

const {
data: apiProviders,
Expand Down
Loading
Loading