Compare commits

...
4 Commits
Author SHA1 Message Date
Eduard Gert 8d6b617cbd Update NextJS to 14.2.35 (#518) 2025-12-22 11:02:29 +01:00
Eduard Gert 47db655e9f Update eslint and tailwind (#515) 2025-11-27 17:38:18 +01:00
dependabot[bot] 0661cbf9f4 Bump js-yaml from 4.1.0 to 4.1.1 (#509)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 15:25:50 +01:00
Eduard Gert 240a96fa8b Add onboarding for new accounts (#514) 2025-11-27 14:49:58 +01:00
26 changed files with 5455 additions and 2851 deletions

No files matched your search

-1
View File
@@ -2,7 +2,6 @@ name: build and push
on:
push:
branches:
- "feature/**"
- main
tags:
- "**"
+2444 -2846
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -56,7 +56,6 @@
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"elkjs": "^0.10.0",
"eslint": "^8",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
@@ -66,7 +65,7 @@
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.539.0",
"next": "^14.2.28",
"next": "^14.2.35",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18.3.1",
@@ -90,9 +89,10 @@
"@faker-js/faker": "^9.5.1",
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint-config-next": "^14.2.28",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.5",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
"tailwindcss": "^3.4.17"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

+6
View File
@@ -17,6 +17,12 @@ declare global {
}
}
export type HubspotFormField = {
objectTypeId?: string;
name: string;
value: string;
};
const AnalyticsContext = React.createContext(
{} as {
initialized: boolean;
+6
View File
@@ -23,4 +23,10 @@ export interface Account {
network_range?: string;
lazy_connection_enabled: boolean;
};
onboarding?: AccountOnboarding;
}
export interface AccountOnboarding {
onboarding_flow_pending: boolean;
signup_form_pending: boolean;
}
+1
View File
@@ -24,6 +24,7 @@ export interface Peer {
login_expiration_enabled: boolean;
inactivity_expiration_enabled: boolean;
approval_required: boolean;
disapproval_reason?: string;
city_name: string;
country_code: string;
connection_ip: string;
+3
View File
@@ -5,6 +5,7 @@ import { useOidcUser } from "@axa-fr/react-oidc";
import Button from "@components/Button";
import { UserAvatar } from "@components/ui/UserAvatar";
import { cn } from "@utils/helpers";
import { isNetBirdHosted } from "@utils/netbird";
import { useIsSm, useIsXs } from "@utils/responsive";
import { AnimatePresence, motion } from "framer-motion";
import { XIcon } from "lucide-react";
@@ -20,6 +21,7 @@ import GroupsProvider from "@/contexts/GroupsProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import UsersProvider from "@/contexts/UsersProvider";
import Navigation from "@/layouts/Navigation";
import { OnboardingProvider } from "@/modules/onboarding/OnboardingProvider";
import Header, { headerHeight } from "./Header";
export default function DashboardLayout({
@@ -33,6 +35,7 @@ export default function DashboardLayout({
<AnnouncementProvider>
<GroupsProvider>
<CountryProvider>
{!isNetBirdHosted() && <OnboardingProvider />}
<DashboardPageContent>{children}</DashboardPageContent>
</CountryProvider>
</GroupsProvider>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,297 @@
import TruncatedText from "@components/ui/TruncatedText";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { cn } from "@utils/helpers";
import {
GlobeIcon,
NetworkIcon,
ShieldCheckIcon,
ShieldXIcon,
WorkflowIcon,
} from "lucide-react";
import * as React from "react";
import RoundedFlag from "@/assets/countries/RoundedFlag";
import { NetworkResource } from "@/interfaces/Network";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer";
import { Intent } from "@/modules/onboarding/Onboarding";
import { OSLogo } from "@/modules/peers/PeerOSCell";
type Props = {
intent?: Intent;
resource?: NetworkResource;
firstDevice?: Peer;
secondDevice?: Peer;
firstRoutingPeer?: Peer;
enabled?: boolean;
};
export const OnboardingDevices = ({
intent,
resource,
firstDevice,
secondDevice,
firstRoutingPeer,
enabled = false,
}: Props) => {
return intent === Intent.P2P ? (
<div
className={cn(
"flex h-full flex-col items-center justify-center text-center text-nb-gray-300 py-8 w-full relative",
!firstDevice && !secondDevice ? "gap-y-8" : "gap-y-2",
)}
>
<DeviceCard device={firstDevice} />
{firstDevice && secondDevice && (
<div
className={cn(
"h-[70px] w-[2px] rounded-full border-l border-dashed border-green-400 relative",
!enabled && "border-nb-gray-600",
)}
></div>
)}
{firstDevice && secondDevice && (
<div
className={
"absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 bg-nb-gray-940 p-2 "
}
>
{enabled ? (
<ShieldCheckIcon size={16} className={"text-green-500"} />
) : (
<ShieldXIcon size={16} className={"text-red-500"} />
)}
</div>
)}
<DeviceCard device={secondDevice} />
{(!firstDevice || !secondDevice) && (
<WaitingForDevice
text={
!firstDevice
? "Waiting for your first device to connect"
: "Waiting for your second device to connect"
}
/>
)}
</div>
) : (
<div
className={cn(
"flex h-full flex-col items-center justify-center text-center text-nb-gray-300 w-full",
"gap-y-2",
firstRoutingPeer && "h-full",
)}
>
{firstRoutingPeer && resource && (
<span className={"text-xs text-nb-gray-500"}>Network</span>
)}
<div
className={cn(
"flex flex-col items-center justify-center gap-y-1",
resource &&
firstRoutingPeer &&
"border px-4 py-5 bg-nb-gray-940 border-nb-gray-900 rounded-lg border-dashed",
)}
>
<DeviceCard resource={resource} />
{resource && (
<Line
className={cn(
firstRoutingPeer && firstDevice && enabled
? "bg-green-400 animate-bg-scroll-faster"
: "bg-nb-gray-600",
)}
height={"30px"}
bg={"#1c1d21"}
config={["4px", "4px", "8px", "7.5px"]}
/>
)}
<DeviceCard device={firstRoutingPeer} />
</div>
<div className={"flex flex-col items-center justify-center relative"}>
{firstRoutingPeer && (
<Line
className={cn(
firstRoutingPeer && firstDevice && enabled
? "bg-green-400 animate-bg-scroll"
: "bg-nb-gray-600",
)}
height={firstDevice && firstRoutingPeer ? "65px" : "25px"}
bg={"#1c1d21"}
/>
)}
<DeviceCard device={firstDevice} />
{(!firstDevice || !firstRoutingPeer) && (
<WaitingForDevice
text={
!firstRoutingPeer
? "Waiting for your routing peer to connect"
: "Waiting for your own device to connect"
}
/>
)}
{firstDevice && firstRoutingPeer && (
<div
className={
"absolute top-0 left-1/2 -translate-x-1/2 bg-nb-gray-940 p-1 mt-[20px]"
}
>
{enabled ? (
<ShieldCheckIcon size={16} className={"text-green-500"} />
) : (
<ShieldXIcon size={16} className={"text-red-500"} />
)}
</div>
)}
</div>
</div>
);
};
export const WaitingForDevice = ({
text = "Waiting for your first device to connect",
}: {
text: string;
}) => {
return (
<div className={"flex flex-col items-center justify-center mt-3"}>
<div className="relative h-10 w-10 mt-4">
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-10 w-10 rounded-full bg-netbird/10 border border-netbird/60 animate-slow-ping "></div>
</div>
<div className="absolute top-1/2 left-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full bg-netbird z-10" />
</div>
<div className="text-sm font-light animate-slow-pulse mt-6">{text}</div>
</div>
);
};
type DeviceCardProps = {
device?: Peer;
resource?: NetworkResource;
};
export const DeviceCard = ({ device, resource }: DeviceCardProps) => {
if (!device && !resource) return;
return (
<div
className={cn(
"flex shrink-0 items-center gap-2.5 text-nb-gray-300 text-left py-1 pl-3 pr-4 rounded-md group/machine my-0 w-[200px]",
)}
>
<div
className={cn(
"flex items-center justify-center rounded-md h-9 w-9 shrink-0 bg-nb-gray-900 transition-all",
"group-hover:bg-nb-gray-800 relative",
)}
>
{device && <PeerOSIcon os={device.os} />}
{resource?.type && <ResourceIcon type={resource.type} />}
{device?.country_code && (
<div className={"absolute -bottom-[4px] -right-[4px]"}>
<div
className={cn(
"flex items-center justify-center rounded-full border-[3px] shrink-0",
"border-nb-gray-940",
)}
>
<RoundedFlag country={device?.country_code} size={10} />
</div>
</div>
)}
</div>
<div className={"flex flex-col gap-0 justify-center mt-2 leading-tight"}>
<span
className={
"mb-1.5 font-normal text-[0.85rem] text-nb-gray-100 flex items-center gap-2"
}
>
<TruncatedText
text={device?.name || resource?.name || "Unknown"}
maxWidth={"150px"}
hideTooltip={true}
/>
</span>
<span
className={
"text-sm font-normal text-nb-gray-400 -top-[0.3rem] relative"
}
>
{device?.ip || resource?.address}
</span>
</div>
</div>
);
};
const PeerOSIcon = ({ os }: { os: string }) => {
const osType = getOperatingSystem(os);
return (
<div
className={cn(
"flex items-center justify-center grayscale brightness-[100%] contrast-[40%]",
"w-4 h-4 shrink-0",
osType === OperatingSystem.WINDOWS && "p-[2.5px]",
osType === OperatingSystem.APPLE && "p-[2.7px]",
osType === OperatingSystem.FREEBSD && "p-[1.5px]",
)}
>
<OSLogo os={os} />
</div>
);
};
const ResourceIcon = ({
type,
size = 15,
}: {
type: "domain" | "host" | "subnet";
size?: number;
}) => {
switch (type) {
case "domain":
return <GlobeIcon size={size} />;
case "subnet":
return <NetworkIcon size={size} />;
case "host":
return <WorkflowIcon size={size} />;
default:
return <WorkflowIcon size={size} />;
}
};
const Line = ({
className,
height = "100%",
bg = "#1c1d21",
config = ["2px", "3px", "6px", "8.2px"],
}: {
className?: string;
height?: string;
bg?: string;
config?: string[];
}) => {
return (
<div
className={cn(
className,
"w-[1px] overflow-hidden relative -left-[0.5px]",
)}
style={{
height: height,
}}
>
<div
className={cn("absolute inset-0 w-full", className)}
style={{
backgroundImage: `repeating-linear-gradient(to bottom, transparent 0%, transparent ${config?.[0]}, ${bg} ${config?.[1]}, ${bg} ${config?.[2]})`,
backgroundSize: `100% ${config?.[3]}`,
}}
/>
</div>
);
};
+131
View File
@@ -0,0 +1,131 @@
import { useOidcUser } from "@axa-fr/react-oidc";
import Button from "@components/Button";
import { ArrowRightIcon, PlayIcon } from "lucide-react";
import Image, { StaticImageData } from "next/image";
import Link from "next/link";
import * as React from "react";
import ACLImage from "@/assets/onboarding/acl.png";
import ActivityImage from "@/assets/onboarding/activity.png";
import PostureCheckImage from "@/assets/onboarding/posture.png";
type Props = {
onFinish?: () => void;
};
export const OnboardingEnd = ({ onFinish }: Props) => {
const { oidcUser: user } = useOidcUser();
const name = user?.given_name || user?.name || user?.preferred_username;
const title = name ? `Congratulations, ${name}!` : "Congratulations!";
return (
<div className={"relative flex flex-col h-full justify-between"}>
<div>
<h1 className={"text-xl text-center max-w-sm mx-auto"}>
{title} <br />
You’ve completed the onboarding.
</h1>
<div
className={
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
}
>
What’s next? Check out these guides to get the most out of NetBird. To
learn more, explore the dashboard, visit our documentation, or browse
our YouTube channel.
</div>
<div className={"mt-8 flex flex-col gap-8"}>
<VideoGuide
title={"Access Control in Under 5 Minutes"}
src={ACLImage}
description={
"Learn how to manage access for your network resources effectively. Whether you want to restrict access to specific machines or allow certain users to connect."
}
href={"https://www.youtube.com/watch?v=WtZD_q-g_Jc"}
/>
<VideoGuide
title={"Provision Users & Groups From Your IdP"}
src={PostureCheckImage}
description={
"Learn how to provision users and groups from your identity provider, such as Okta, Azure AD, or Google Workspace, to manage access control in NetBird and automate onboarding and offboarding processes."
}
href={"https://www.youtube.com/watch?v=RxYWTpf7cgY"}
/>
<VideoGuide
title={"How NetBird Works"}
description={
"Learn more about how NetBird works, its architecture, and how it can help you build secure networks."
}
src={ActivityImage}
href={"https://www.youtube.com/watch?v=CFa7SY4Up9k&t=261s"}
/>
</div>
<div className={"mt-10 flex items-center justify-center"}>
<Button variant={"secondaryLighter"} onClick={onFinish}>
Go to Dashboard
<ArrowRightIcon size={16} />
</Button>
</div>
</div>
</div>
);
};
type VideoGuideProps = {
src?: string | StaticImageData;
title?: string;
description?: string;
href?: string;
};
const VideoGuide = ({
src = ACLImage,
title = "Access Control in Under 5 Minutes",
description = "Learn how to manage access for your network resources effectively. Whether you want to restrict access to specific machines or allow certain users to connect.",
href = "#",
}: VideoGuideProps) => {
return (
<div
className={
"flex flex-col sm:flex-row gap-3 items-center text-center sm:text-left sm:gap-6"
}
>
<Link
className={
"border border-nb-gray-900 rounded-lg p-[2px] bg-nb-gray-920 min-w-[160px] max-w-[160px] relative group hover:bg-nb-gray-900 transition-all"
}
target={"_blank"}
href={href}
>
<span
className={
"flex items-center justify-center absolute left-0 top-0 h-full w-full"
}
>
<div
className={
"bg-nb-gray-900/50 group-hover:bg-nb-gray-600/50 backdrop-blur h-8 w-8 flex items-center justify-center rounded-full"
}
>
<PlayIcon size={14} />
</div>
</span>
<Image
src={src}
alt={title}
className={"border border-nb-gray-900 rounded-md"}
/>
</Link>
<div>
<div className={"text-md"}>{title}</div>
<div
className={"text-[0.8rem] text-nb-gray-300 font-light mt-1.5 block"}
>
{description}
</div>
</div>
</div>
);
};
+176
View File
@@ -0,0 +1,176 @@
import FullTooltip from "@components/FullTooltip";
import {IconArrowRight} from "@tabler/icons-react";
import {cn} from "@utils/helpers";
import {HelpCircle} from "lucide-react";
import * as React from "react";
import {useMemo} from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import {Intent} from "@/modules/onboarding/Onboarding";
type Props = {
onSelect: (intent: Intent) => void,
useCases?: string,
isBusiness?: boolean
};
export const OnboardingIntent = ({onSelect, useCases, isBusiness}: Props) => {
/**
* Recommend Networks if users ticks any of these use cases
*/
const isNetworksRecommended = useMemo(() => {
if (!useCases) return false;
const intents = [
"Zero Trust Security",
"Employee Remote Access",
"Business VPN",
"Site-to-Site Connectivity",
"IoT (Internet of Things)",
"MSP (Managed Service Provider)",
];
for (const intent of intents) {
if (useCases.toLowerCase().includes(intent.toLowerCase())) {
return true;
}
}
return false;
}, [useCases]);
/**
* Recommend P2P if users ticks any of these use cases
*/
const isP2PRecommended = useMemo(() => {
if (!useCases) return false;
const intents = [
"Homelab Automation",
"Home Remote Access",
"File Access",
"Gaming",
];
for (const intent of intents) {
if (useCases.toLowerCase().includes(intent.toLowerCase())) {
return true;
}
}
return false;
}, [useCases]);
return (
<div className={"relative flex flex-col h-full justify-between"}>
<div>
<h1 className={"text-xl text-center"}>Get started with NetBird</h1>
<div
className={
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
}
>
NetBird provides the flexibility of both a peer-to-peer overlay network and a remote network access
solution.
Choose what fits your needs, you can always combine both.
</div>
<div
className={cn(
"grid grid-cols-1 mt-8",
"border border-nb-gray-900 rounded-lg flex items-start flex-col relative bg-nb-gray-930/60 transition-all ",
)}
>
<IntentCard
title={"Peer-to-Peer Network"}
description={
isBusiness ? "Install NetBird on two or more devices to create secure, direct WireGuard connections, like laptop to server or server to database. Add at least two machines to get started." :"Install NetBird on two or more devices in your homelab, such as your laptop, NAS, or Raspberry Pi, to create secure, direct WireGuard connections."
}
recommended={isP2PRecommended}
icon={<PeerIcon size={18} className={"fill-netbird"}/>}
onClick={() => onSelect(Intent.P2P)}
/>
<IntentCard
title={"Remote Network Access"}
description={
isBusiness ? "Enable employee remote access to VMs, Kubernetes clusters, and cloud or on-prem resources without installing NetBird on every machine." : "Securely access your homelab remotely from anywhere without installing NetBird on every device."
}
recommended={isNetworksRecommended}
icon={<NetworkRoutesIcon size={18} className={"fill-netbird"}/>}
onClick={() => onSelect(Intent.NETWORKS)}
/>
</div>
</div>
</div>
);
};
type IntentCardProps = {
title: string;
description: string;
icon: React.ReactNode;
onClick: () => void;
recommended?: boolean;
};
const IntentCard = ({
title,
description,
icon,
onClick,
recommended,
}: IntentCardProps) => {
return (
<button
className={
"px-6 py-6 flex items-start flex-col relative hover:bg-nb-gray-920 transition-all group first:border-b border-nb-gray-900"
}
onClick={onClick}
>
<div className={"flex gap-6"}>
<div
className={cn(
"h-10 w-10 flex items-center justify-center rounded-md shrink-0 mt-2",
"bg-nb-gray-900 border border-nb-gray-800 ",
)}
>
{icon}
</div>
<div className={"flex gap-4 items-center"}>
<div className={"text-left"}>
<h2
className={
"text-base font-medium mb-.5 group-hover:text-netbird transition-all inline-flex gap-x-2 gap-y-1 flex-wrap"
}
>
{title}
{recommended && (
<FullTooltip
content={
<div className={"text-xs max-w-xs"}>
Based on your previous choices, we recommend starting with{" "}
{title}. You can always combine both options later.
</div>
}
>
<span
className={cn(
"relative",
"inline-flex text-[0.7rem] font-light bg-netbird/10 border border-netbird-400/30 text-netbird-400 rounded-full px-2 py-1 pb-0.5 leading-none",
"hover:bg-netbird/20 cursor-help transition-all self-center",
)}
>
Recommended
<HelpCircle size={10} className={"ml-1"}/>
</span>
</FullTooltip>
)}
</h2>
<p className={"!text-nb-gray-300 text-[.85rem]"}>{description}</p>
</div>
<div
className={"h-full items-center text-nb-gray-400 hidden sm:flex"}
>
<IconArrowRight
size={24}
className={"shrink-0 group-hover:text-netbird"}
/>
</div>
</div>
</div>
</button>
);
};
@@ -0,0 +1,39 @@
import { ToggleSwitch } from "@components/ToggleSwitch";
import { cn } from "@utils/helpers";
import { ShieldIcon } from "lucide-react";
import * as React from "react";
import { Policy } from "@/interfaces/Policy";
type Props = {
policy?: Policy;
onToggle?: (policy: Policy) => void;
};
export const OnboardingPolicy = ({ policy, onToggle }: Props) => {
if (!policy) return;
return (
<label
className={cn(
"relative block rounded-lg border border-nb-gray-900 px-5 py-3 transition-all",
"flex justify-between items-center mt-3 cursor-pointer",
)}
>
<div>
<div className="text-nb-gray-100 font-normal text-sm text-left gap-2 flex items-center">
<ShieldIcon size={12} className={"shrink-0"} />
{policy?.name} Policy
</div>
<div className={"text-nb-gray-300 text-[0.8rem] text-left mt-0.5"}>
{policy?.name.includes("Default")
? "Allows connections between all your devices"
: policy?.description}
</div>
</div>
<ToggleSwitch
onCheckedChange={() => onToggle?.(policy)}
checked={policy?.enabled || false}
/>
</label>
);
};
@@ -0,0 +1,170 @@
import { useLocalStorage } from "@hooks/useLocalStorage";
import useFetchApi, { useApiCall } from "@utils/api";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import { useRouter, useSearchParams } from "next/navigation";
import { useMemo } from "react";
import { useSWRConfig } from "swr";
import { HubspotFormField, useAnalytics } from "@/contexts/AnalyticsProvider";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Account } from "@/interfaces/Account";
import { Network } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
import { useAccount } from "@/modules/account/useAccount";
import {
Intent,
Onboarding,
OnboardingState,
} from "@/modules/onboarding/Onboarding";
type Props = {
onSurveySubmit?: (data: {
fields: HubspotFormField[];
hsId: string;
gaId: string;
accountId?: string;
userId?: string;
}) => void;
domainCategory?: string;
};
export const OnboardingProvider = ({
onSurveySubmit,
domainCategory,
}: Props) => {
const { data: peers } = useFetchApi<Peer[]>("/peers");
const accountRequest = useApiCall<Account>("/accounts", true);
const account = useAccount();
const router = useRouter();
const { isOwner, loggedInUser } = useLoggedInUser();
const { mutate } = useSWRConfig();
const { trackEventV2 } = useAnalytics();
const params = useSearchParams();
const hsId = params?.get("hs_id") ?? "";
const gaId = params?.get("ga_id") ?? "";
const accountId = account?.id ?? "unknown";
const onboardingKey = `netbird-onboarding-flow:${accountId}`;
// Migrate old onboarding state to new key if needed
if (typeof window !== "undefined" && account?.id) {
const oldKey = "netbird-onboarding-flow";
const oldValue = window.localStorage.getItem(oldKey);
const newValue = window.localStorage.getItem(onboardingKey);
if (oldValue && !newValue) {
window.localStorage.setItem(onboardingKey, oldValue);
window.localStorage.removeItem(oldKey);
}
}
const [onboarding, setOnboarding] = useLocalStorage<OnboardingState>(
onboardingKey,
{
intent: Intent.P2P,
step: 1,
},
);
const showOnboarding = useMemo(() => {
if (process.env.APP_ENV === "test") return false;
if (!account) return false;
const isSignupFormPending = isNetBirdHosted()
? !!account?.onboarding?.signup_form_pending
: false;
const show =
!!account?.onboarding?.onboarding_flow_pending || isSignupFormPending;
return isOwner && show;
}, [account, isOwner]);
const updateAccountMeta = async (meta: Partial<Account["onboarding"]>) => {
if (!account) return;
await accountRequest
.put(
{
...account,
id: account.id,
onboarding: {
...account.onboarding,
...meta,
},
},
`/${account.id}`,
)
.then(() => mutate("/accounts"));
};
const onSkip = async (intent: Intent, step: number) => {
await updateAccountMeta({
onboarding_flow_pending: false,
});
trackEventV2(
"Onboarding",
`Skipped Onboarding - ${intent} (Step ${step})`,
account?.id,
loggedInUser?.id,
);
};
const onFinish = async (n?: Network) => {
await updateAccountMeta({
onboarding_flow_pending: false,
});
trackEventV2(
"Onboarding",
"Finished Onboarding",
account?.id,
loggedInUser?.id,
);
if (n) {
// router.push(`/network?id=${n.id}`);
router.push("/control-center?tab=networks");
} else {
router.push("/control-center");
}
};
const onTroubleshootingClick = (intent: Intent) => {
trackEventV2(
"Onboarding",
`Troubleshooting - ${intent}`,
account?.id,
loggedInUser?.id,
);
};
const submitSurvey = async (fields: HubspotFormField[]) => {
await updateAccountMeta({
signup_form_pending: false,
});
if (isLocalDev()) return;
onSurveySubmit?.({
fields,
hsId,
gaId,
accountId: account?.id,
userId: loggedInUser?.id,
});
};
const formSubmitted = isNetBirdHosted()
? !account?.onboarding?.signup_form_pending
: true;
return (
<>
{showOnboarding && peers && (
<Onboarding
formSubmitted={formSubmitted}
isOnboardingPending={!!account?.onboarding?.onboarding_flow_pending}
initial={onboarding}
setLocalOnboarding={setOnboarding}
peers={peers}
onSurveySubmit={submitSurvey}
onTroubleshootingClick={onTroubleshootingClick}
onSkip={onSkip}
onFinish={onFinish}
domainCategory={domainCategory}
/>
)}
</>
);
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,259 @@
import Button from "@components/Button";
import { notify } from "@components/Notification";
import { RadioCard, RadioCardGroup } from "@components/RadioCard";
import { useApiCall } from "@utils/api";
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
import * as React from "react";
import { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import { useGroups } from "@/contexts/GroupsProvider";
import { Group } from "@/interfaces/Group";
import { Network, NetworkResource } from "@/interfaces/Network";
import { Policy } from "@/interfaces/Policy";
import { ResourceSingleAddressInput } from "@/modules/networks/resources/ResourceSingleAddressInput";
type Props = {
onNetworkCreation?: (network: Network) => void;
onResourceCreation?: (resource: NetworkResource) => void;
onBack: () => void;
};
export const OnboardingAddResource = ({
onNetworkCreation,
onResourceCreation,
onBack,
}: Props) => {
const [resourceType, setResourceType] = useState("");
const [resourceAddress, setResourceAddress] = useState("");
const [error, setError] = useState("");
const [network, setNetwork] = useState<Network>();
const { mutate } = useSWRConfig();
const { groups } = useGroups();
const networkRequest = useApiCall<Network>("/networks", true);
const resourceRequest = useApiCall<NetworkResource>("/networks", true);
const policyRequest = useApiCall<Policy>("/policies", true);
const groupRequest = useApiCall<Group>("/groups", true);
const allGroupId = groups?.find((g) => g.name === "All")?.id;
/**
* Create a new network and add a resource to it
*/
const createResource = async () => {
let myNetwork = network;
if (!network) {
await networkRequest
.post({
name: "My First Network",
description: "Created during onboarding",
})
.then((n) => {
myNetwork = n;
onNetworkCreation?.(n);
setNetwork(n);
});
}
if (!myNetwork) return;
notify({
title: "My First Network",
description: "Network & Resource created successfully",
loadingMessage: "Creating your resource...",
promise: resourceRequest
.post(
{
name: resourceType === "subnet" ? "My Subnet" : "My Resource",
description: "Created during onboarding",
address: resourceAddress,
enabled: true,
groups: [],
},
`/${myNetwork.id}/resources`,
)
.then((r) => {
onResourceCreation?.(r);
createOnboardingGroups().then(({ usersGroup, routingPeersGroup }) => {
createUsersToResourcePolicy(r, usersGroup);
createUsersToRoutingPeersPolicy(r, usersGroup, routingPeersGroup);
});
}),
});
};
/**
* Create Users and Routing Peers groups if they do not exist
*/
const createOnboardingGroups = async () => {
let usersGroup = groups?.find((group) => group.name === "Users");
let routingPeersGroup = groups?.find(
(group) => group.name === "Routing Peers",
);
if (!usersGroup) {
usersGroup = await groupRequest.post({
name: "Users",
});
}
if (!routingPeersGroup) {
routingPeersGroup = await groupRequest.post({
name: "Routing Peers",
});
}
return {
usersGroup,
routingPeersGroup,
};
};
/**
* Create a policy that allows users to access the resource
*/
const createUsersToResourcePolicy = async (
r: NetworkResource,
usersGroup: Group,
) => {
const isSubnet = r.type === "subnet";
await policyRequest.post({
name: `Users to ${r.name}`,
description: `Allows access to this ${
isSubnet ? `subnet ${r.address}` : `resource ${r.address}`
}`,
enabled: true,
rules: [
{
name: `Users to ${r.name}`,
description: `Allows access to this ${
isSubnet ? `subnet ${r.address}` : `resource ${r.address}`
}`,
enabled: true,
action: "accept",
bidirectional: true,
protocol: "all",
sources: usersGroup ? [usersGroup.id] : [allGroupId],
destinationResource: {
type: r.type,
id: r.id,
},
},
],
});
};
/**
* Create a policy that allows users to access routing peers
*/
const createUsersToRoutingPeersPolicy = async (
r: NetworkResource,
usersGroup: Group,
routingPeersGroup: Group,
) => {
await policyRequest
.post({
name: `Users to Routing Peers`,
description: `Allows users to access routing peers`,
enabled: true,
rules: [
{
name: `Users to Routing Peers`,
description: `Allows users to access routing peers`,
enabled: true,
action: "accept",
bidirectional: true,
protocol: "all",
sources: usersGroup ? [usersGroup.id] : [allGroupId],
destinations: routingPeersGroup
? [routingPeersGroup.id]
: [allGroupId],
},
],
})
.then(() => {
mutate("/policies");
mutate("/groups");
});
};
const description = useMemo(() => {
if (resourceType === "ip")
return "Enter a single IPv4 address of your resource";
if (resourceType === "subnet") return "Enter a CIDR range of your network";
if (resourceType === "domain")
return "Enter a domain name of your resource";
}, [resourceType]);
const placeholder = useMemo(() => {
if (resourceType === "ip") return "e.g., 192.168.31.45";
if (resourceType === "subnet") return "e.g., 192.168.1.0/24";
if (resourceType === "domain")
return "e.g., service.internal or *.services.internal";
}, [resourceType]);
return (
<div className={"relative flex flex-col h-full gap-4"}>
<div className={"flex flex-col gap-8"}>
<div>
<h1 className={"text-xl text-center"}>Add your first resource</h1>
<div
className={
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
}
>
Resources are your subnets, services, or machines inside your network.
Pick the type you want to connect to.
</div>
</div>
<RadioCardGroup value={resourceType} onValueChange={setResourceType}>
<RadioCard
value={"ip"}
title={"Single IP Address"}
icon={<WorkflowIcon size={12} />}
description={"IPv4 address like 192.168.31.45"}
/>
<RadioCard
value={"subnet"}
title={"Entire Subnet"}
icon={<NetworkIcon size={12} />}
description={"CIDR range like 192.168.0.0/24"}
/>
<RadioCard
value={"domain"}
title={"Domain"}
icon={<GlobeIcon size={12} />}
description={
"A domain like service.internal or a wildcard like *.services.internal"
}
/>
</RadioCardGroup>
{resourceType && (
<ResourceSingleAddressInput
label={"What is the address of your resource?"}
value={resourceAddress}
onChange={setResourceAddress}
onError={setError}
description={description}
placeholder={placeholder}
/>
)}
<div className={"flex gap-4"}>
<Button variant={"secondary"} className={"w-full"} onClick={onBack}>
Go Back
</Button>
<Button
variant={"primary"}
className={"w-full"}
onClick={createResource}
disabled={resourceAddress === "" || error !== ""}
>
Create Resource
</Button>
</div>
</div>
</div>
);
};
@@ -0,0 +1,185 @@
import Button from "@components/Button";
import { Modal, ModalContent } from "@components/modal/Modal";
import { notify } from "@components/Notification";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import { CopyIcon, DownloadIcon, KeyRoundIcon } from "lucide-react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { Group } from "@/interfaces/Group";
import { Network, NetworkRouter } from "@/interfaces/Network";
import { Peer } from "@/interfaces/Peer";
import { SetupKey } from "@/interfaces/SetupKey";
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
type Props = {
network?: Network;
peers?: Peer[];
onRoutingPeerAdded: (peer: Peer) => void;
};
export const OnboardingAddRoutingPeer = ({
network,
peers,
onRoutingPeerAdded,
}: Props) => {
const [open, setOpen] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const { groups } = useGroups();
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
const groupRequest = useApiCall<Group>("/groups", true);
const routerRequest = useApiCall<NetworkRouter>("/networks", true);
/**
* Generate a new setup key for the routing peer
*/
const generateSetupKey = async () => {
let routingPeerGroup = groups?.find(
(group) => group.name === "Routing Peers",
);
if (!routingPeerGroup) {
routingPeerGroup = await groupRequest.post({
name: "Routing Peers",
});
}
notify({
title: "Setup Key Created",
description: "Successfully copied to clipboard.",
loadingMessage: "Generating setup key...",
promise: setupKeyRequest
.post({
name: "Routing Peer (My First Network)",
type: "one-off",
expires_in: 24 * 60 * 60, // 1 day expiration
revoked: false,
auto_groups: routingPeerGroup ? [routingPeerGroup.id] : [],
usage_limit: 1,
ephemeral: false,
allow_extra_dns_labels: false,
})
.then((setupKey) => {
setSetupKey(setupKey);
copySetupKey(setupKey.key);
}),
});
};
/**
* Detect routing peer based on group and add it to the network
*/
useEffect(() => {
const routingPeer = peers?.find(
(p) => p.groups?.some((g) => g.name === "Routing Peers"),
);
const hasNetworkRoutingPeer =
network?.routers?.find((r) => r === routingPeer?.id) !== undefined;
if (routingPeer && network && !hasNetworkRoutingPeer) {
routerRequest
.post(
{
peer: routingPeer.id,
metric: 9999,
masquerade: true,
enabled: true,
},
`/${network.id}/routers`,
)
.then(() => {
onRoutingPeerAdded(routingPeer);
});
}
}, [network, peers]);
/**
* Copy the setup key to clipboard
*/
const copySetupKey = async (key: string, showMessage = false) => {
try {
await navigator.clipboard.writeText(key || "");
if (showMessage) {
notify({
title: "Setup Key Copied",
description: "Successfully copied to clipboard.",
});
}
} catch (e) {}
};
return (
<div className={"relative flex flex-col h-full gap-4"}>
<div>
<h1 className={"text-xl text-center"}>
Add a routing peer and get the traffic flowing
</h1>
<div
className={
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
}
>
Think of a routing peer as a connector to your internal network.
It runs NetBird and lets your remote devices access internal resources, while enforcing access control policies.
</div>
<div
className={
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
}
>
Generate a setup key and install NetBird on that machine.
</div>
</div>
<div
className={cn(
"relative block rounded-lg border border-nb-gray-900 px-5 py-3 transition-all",
"flex justify-between items-center mt-3",
)}
>
<div>
<div className="text-nb-gray-100 font-normal text-sm text-left gap-2 flex items-center">
<KeyRoundIcon size={12} />
Setup-Key
</div>
<div className={"text-nb-gray-300 text-[0.8rem] text-left mt-0.5"}>
{setupKey?.key || "Not yet generated"}
</div>
</div>
{setupKey ? (
<Button
variant={"secondary"}
onClick={() => copySetupKey(setupKey.key, true)}
>
<CopyIcon size={14} />
</Button>
) : (
<Button variant={"primary"} onClick={generateSetupKey} size={"xs"}>
Generate Setup Key
</Button>
)}
</div>
<Button
variant={"primary"}
className={""}
disabled={!setupKey}
onClick={() => setOpen(true)}
>
<DownloadIcon size={16} />
Install Routing Peer
</Button>
{setupKey && (
<Modal open={open} onOpenChange={setOpen}>
<ModalContent>
<SetupModalContent
hostname={"routing-peer"}
title={"Install NetBird"}
setupKey={setupKey.key}
/>
</ModalContent>
</Modal>
)}
</div>
);
};
@@ -0,0 +1,95 @@
import Button from "@components/Button";
import { Modal, ModalContent } from "@components/modal/Modal";
import { useApiCall } from "@utils/api";
import { DownloadIcon } from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import { Group, GroupPeer } from "@/interfaces/Group";
import { Peer } from "@/interfaces/Peer";
import { Policy } from "@/interfaces/Policy";
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
type Props = {
device?: Peer;
policy?: Policy;
onNext?: () => void;
};
export const OnboardingAddUserDevice = ({ device, policy, onNext }: Props) => {
const groupRequest = useApiCall<Group>("/groups", true);
const { mutate } = useSWRConfig();
const [open, setOpen] = useState(false);
const usersGroup = useMemo(() => {
let rule = policy?.rules?.[0];
const sourceGroups = rule?.sources as Group[];
return sourceGroups?.find((g) => g.name === "Users");
}, [policy]);
const hasDeviceUsersGroup = device?.groups?.find((g) => g.name === "Users");
/**
* Detect the device and add it to the "Users" group
*/
useEffect(() => {
if (!hasDeviceUsersGroup && usersGroup && device) {
let peersOfGroup = (usersGroup.peers as GroupPeer[]) || [];
let newPeers = peersOfGroup
.map((p) => p.id)
.filter((x) => x !== undefined);
if (device?.id) newPeers.push(device.id);
groupRequest
.put(
{
...usersGroup,
peers: newPeers,
},
`/${usersGroup.id}`,
)
.then(() => {
mutate("/peers");
mutate("/groups");
});
}
}, [usersGroup, device, hasDeviceUsersGroup]);
/**
* Continue to next step once device is recognized
*/
useEffect(() => {
if (device && hasDeviceUsersGroup) {
onNext?.();
}
}, [device, hasDeviceUsersGroup]);
return (
<div className={"relative flex flex-col h-full gap-4"}>
<div>
<h1 className={"text-xl text-center max-w-sm mx-auto"}>
{"Time to add your client device"}
</h1>
<div
className={
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
}
>
{`Your first resource and routing peer are all set. Now, take your device, install NetBird, and let's get you connected.`}
</div>
</div>
<div className={"flex items-center justify-center mt-3"}>
<Button variant={"primary"} onClick={() => setOpen(true)}>
<DownloadIcon size={16} />
Install NetBird
</Button>
</div>
<Modal open={open} onOpenChange={setOpen}>
<ModalContent>
<SetupModalContent title={"Install NetBird"} hideDocker={true} />
</ModalContent>
</Modal>
</div>
);
};
Loaded 20 of 26 files, more files were not shown because too many files have changed in this diff. Show more