mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92528d8377 | ||
|
|
876f803033 | ||
|
|
43286352d1 | ||
|
|
8d6b617cbd | ||
|
|
47db655e9f | ||
|
|
0661cbf9f4 | ||
|
|
240a96fa8b | ||
|
|
43bc069a49 | ||
|
|
936de0f4f3 | ||
|
|
d81b75a946 |
No files matched your search
@@ -2,7 +2,6 @@ name: build and push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature/**"
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
|
||||
Generated
+2444
-2846
File diff suppressed because it is too large.
Load diff
+4
-5
@@ -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,10 +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",
|
||||
"cypress": "^13.13.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.5",
|
||||
"postcss": "^8",
|
||||
"prettier": "3.0.3",
|
||||
"tailwindcss": "^3"
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,7 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
if (!peer.id) return;
|
||||
if (connected.current) return;
|
||||
connected.current = true;
|
||||
|
||||
try {
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const rules = [`tcp/${aclPort}`];
|
||||
@@ -121,7 +122,7 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
sshConnectedOnce.current = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Connection failed:", error);
|
||||
console.error("Connection error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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 |
@@ -17,6 +17,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export type HubspotFormField = {
|
||||
objectTypeId?: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const AnalyticsContext = React.createContext(
|
||||
{} as {
|
||||
initialized: boolean;
|
||||
|
||||
@@ -4,7 +4,18 @@ import md5 from "crypto-js/md5";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
|
||||
const initialAnnouncements: Announcement[] = [];
|
||||
const initialAnnouncements: Announcement[] = [
|
||||
{
|
||||
tag: "New",
|
||||
text: "NetBird v0.60.0 - Identity-aware, private SSH over your NetBird network.",
|
||||
link: "https://docs.netbird.io/how-to/ssh",
|
||||
linkText: "Documentation",
|
||||
variant: "default", // "default" or "important"
|
||||
isExternal: true,
|
||||
closeable: true,
|
||||
isCloudOnly: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface Announcement extends AnnouncementVariant {
|
||||
tag: string;
|
||||
|
||||
@@ -138,6 +138,7 @@ export default function PeerProvider({
|
||||
<PeerSSHInstructions
|
||||
open={sshInstructionsModal}
|
||||
onOpenChange={setSSHInstructionsModal}
|
||||
peer={peer}
|
||||
onSuccess={() => toggleSSH(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface User {
|
||||
pending_approval?: boolean;
|
||||
last_login?: Date;
|
||||
permissions: Permissions;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -45,7 +45,7 @@ import React, { useMemo, useState } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Policy, Protocol } from "@/interfaces/Policy";
|
||||
import { Policy, PolicyRuleResource, Protocol } from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { useAccessControl } from "@/modules/access-control/useAccessControl";
|
||||
import { PostureCheckTab } from "@/modules/posture-checks/ui/PostureCheckTab";
|
||||
@@ -116,6 +116,9 @@ type ModalProps = {
|
||||
postureCheckTemplates?: PostureCheck[];
|
||||
useSave?: boolean;
|
||||
allowEditPeers?: boolean;
|
||||
initialProtocol?: Protocol;
|
||||
initialPorts?: number[];
|
||||
initialDestinationResource?: PolicyRuleResource;
|
||||
};
|
||||
|
||||
export function AccessControlModalContent({
|
||||
@@ -128,6 +131,9 @@ export function AccessControlModalContent({
|
||||
initialDestinationGroups,
|
||||
initialName,
|
||||
initialDescription,
|
||||
initialProtocol,
|
||||
initialPorts,
|
||||
initialDestinationResource,
|
||||
}: Readonly<ModalProps>) {
|
||||
const { permission } = usePermissions();
|
||||
|
||||
@@ -170,6 +176,9 @@ export function AccessControlModalContent({
|
||||
initialDestinationGroups,
|
||||
initialName,
|
||||
initialDescription,
|
||||
initialPorts,
|
||||
initialProtocol,
|
||||
initialDestinationResource,
|
||||
});
|
||||
|
||||
const [tab, setTab] = useState(() => {
|
||||
|
||||
@@ -6,7 +6,12 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { usePolicies } from "@/contexts/PoliciesProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Policy, PortRange, Protocol } from "@/interfaces/Policy";
|
||||
import {
|
||||
Policy,
|
||||
PolicyRuleResource,
|
||||
PortRange,
|
||||
Protocol,
|
||||
} from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { usePostureCheck } from "@/modules/posture-checks/usePostureCheck";
|
||||
@@ -18,6 +23,9 @@ type Props = {
|
||||
initialDestinationGroups?: Group[] | string[];
|
||||
initialName?: string;
|
||||
initialDescription?: string;
|
||||
initialProtocol?: Protocol;
|
||||
initialPorts?: number[];
|
||||
initialDestinationResource?: PolicyRuleResource;
|
||||
};
|
||||
|
||||
// TODO add reducer
|
||||
@@ -29,6 +37,9 @@ export const useAccessControl = ({
|
||||
initialName,
|
||||
initialDescription,
|
||||
onSuccess,
|
||||
initialProtocol,
|
||||
initialPorts,
|
||||
initialDestinationResource,
|
||||
}: Props = {}) => {
|
||||
const { data: allPostureChecks, isLoading: isPostureChecksLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
@@ -75,6 +86,7 @@ export const useAccessControl = ({
|
||||
const [enabled, setEnabled] = useState<boolean>(policy?.enabled ?? true);
|
||||
|
||||
const [ports, setPorts] = useState<number[]>(() => {
|
||||
if (initialPorts) return initialPorts;
|
||||
if (!firstRule) return [];
|
||||
if (firstRule.ports == undefined) return [];
|
||||
if (firstRule.ports.length > 0) {
|
||||
@@ -93,7 +105,7 @@ export const useAccessControl = ({
|
||||
});
|
||||
|
||||
const [protocol, setProtocol] = useState<Protocol>(
|
||||
firstRule ? firstRule.protocol : "all",
|
||||
firstRule ? firstRule.protocol : initialProtocol ?? "all",
|
||||
);
|
||||
const [direction, setDirection] = useState<Direction>(() => {
|
||||
if (!firstRule) return "bi";
|
||||
@@ -131,7 +143,7 @@ export const useAccessControl = ({
|
||||
);
|
||||
|
||||
const [destinationResource, setDestinationResource] = useState(
|
||||
firstRule?.destinationResource,
|
||||
firstRule?.destinationResource ?? initialDestinationResource,
|
||||
);
|
||||
|
||||
const { updateOrCreateAndNotify: checkToCreate } = usePostureCheck({});
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Loaded 20 of 45 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user