Remove integrations from public repo and sync changes (#369)

* Change icon size

* Remove integrations

* Add no cache header

* Add analytics event tracking

* Add small announcement improvements

* Remove peer approval setting

* Do not load countries when user has no permission

* Add tab query params to settings

* Decrease navigation font size

* Change order of providers

* Increase padding for modals

* Show page only when user is fully loaded and found

* Remove unused state

* Remove integrations page
This commit is contained in:
Eduard Gert
2024-04-02 14:06:38 +02:00
committed by GitHub
parent 859916b1df
commit 6d4716cdad
50 changed files with 176 additions and 4252 deletions

View File

@@ -1,4 +1,3 @@
# simple server configuration to replace nginx's default
server {
listen 80 default_server;
listen [::]:80 default_server;
@@ -7,10 +6,14 @@ server {
location / {
try_files $uri $uri.html $uri/ =404;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
}

View File

@@ -5,14 +5,12 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import ActivityIcon from "@/assets/icons/ActivityIcon";
import { ActivityEvent } from "@/interfaces/ActivityEvent";
import PageContainer from "@/layouts/PageContainer";
import ActivityTable from "@/modules/activity/ActivityTable";
import { EventStreamingCard } from "@/modules/integrations/event-streaming/EventStreamingCard";
export default function Activity() {
const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events");
@@ -50,7 +48,6 @@ export default function Activity() {
</Paragraph>
</div>
<RestrictedAccess page={"Activity"}>
{(isLocalDev() || isNetBirdHosted()) && <EventStreamingCard />}
<ActivityTable events={events} isLoading={isLoading} />
</RestrictedAccess>
</PageContainer>

View File

@@ -1,8 +0,0 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Integrations - ${globalMetaTitle}`,
};
export default BlankLayout;

View File

@@ -1,39 +0,0 @@
"use client";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { VerticalTabs } from "@components/VerticalTabs";
import { FileText, FingerprintIcon } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useState } from "react";
import PageContainer from "@/layouts/PageContainer";
import EventStreamingTab from "@/modules/integrations/event-streaming/EventStreamingTab";
import IdentityProviderTab from "@/modules/integrations/idp-sync/IdentityProviderTab";
export default function Integrations() {
const searchParams = useSearchParams();
const currentTab = searchParams.get("tab");
const [tab, setTab] = useState(currentTab || "event-streaming");
return (
<PageContainer>
<VerticalTabs value={tab} onChange={setTab}>
<VerticalTabs.List>
<VerticalTabs.Trigger value="event-streaming">
<FileText size={14} />
Event Streaming
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="identity-provider">
<FingerprintIcon size={14} />
Identity Provider
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess page={"Integrations"}>
<div className={"border-l border-nb-gray-930 w-full"}>
<EventStreamingTab />
<IdentityProviderTab />
</div>
</RestrictedAccess>
</VerticalTabs>
</PageContainer>
);
}

View File

@@ -8,7 +8,8 @@ import {
LockIcon,
ShieldIcon,
} from "lucide-react";
import React, { useState } from "react";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import PageContainer from "@/layouts/PageContainer";
import { useAccount } from "@/modules/account/useAccount";
@@ -18,10 +19,18 @@ import GroupsTab from "@/modules/settings/GroupsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab";
export default function NetBirdSettings() {
const [tab, setTab] = useState("authentication");
const queryParams = useSearchParams();
const queryTab = queryParams.get("tab");
const [tab, setTab] = useState(queryTab || "authentication");
const { isOwner } = useLoggedInUser();
const account = useAccount();
useEffect(() => {
if (queryTab) {
setTab(queryTab);
}
}, [queryTab]);
return (
<PageContainer>
<VerticalTabs value={tab} onChange={setTab}>

View File

@@ -5,7 +5,7 @@ export type IconProps = {
};
export const defaultIconProps: IconProps = {
size: 16,
size: 15,
className:
"dark:fill-nb-gray-400 fill-gray-500 peer-data-[active=true]/icon:dark:fill-white peer-data-[active=true]/icon:fill-gray-900 shrink-0",
autoHeight: false,

View File

@@ -60,7 +60,7 @@ export default function SidebarItem({
<li className={"px-4 cursor-pointer"}>
<button
className={classNames(
"rounded-lg text-base w-full ",
"rounded-lg text-[.95rem] w-full ",
"font-normal ",
className,
isChild ? "pl-7 pr-2 py-2 mt-1 mb-0.5" : "py-2 px-3",

View File

@@ -19,6 +19,7 @@ const AnalyticsContext = React.createContext(
{} as {
initialized: boolean;
trackPageView: () => void;
trackEvent: (category: string, action: string, label: string) => void;
},
);
const config = loadConfig();
@@ -51,8 +52,20 @@ export default function AnalyticsProvider({ children }: Props) {
ReactGA.send({ hitType: "pageview", page: path, title: document.title });
};
const trackEvent = (category: string, action: string, label: string) => {
if (isProduction() && ReactGA.isInitialized) {
ReactGA.event({
category: category,
action: action,
label: label,
});
}
};
return (
<AnalyticsContext.Provider value={{ initialized, trackPageView }}>
<AnalyticsContext.Provider
value={{ initialized, trackPageView, trackEvent }}
>
{children}
</AnalyticsContext.Provider>
);

View File

@@ -13,6 +13,7 @@ export interface Announcement extends AnnouncementVariant {
linkText?: string;
isExternal?: boolean;
closeable: boolean;
isCloudOnly: boolean;
}
interface AnnouncementInfo extends Announcement {
@@ -29,6 +30,9 @@ const AnnouncementContext = React.createContext(
bannerHeight: number;
announcements?: AnnouncementInfo[];
closeAnnouncement: (hash: string) => void;
setAnnouncements: React.Dispatch<
React.SetStateAction<AnnouncementInfo[] | undefined>
>;
},
);
@@ -43,6 +47,7 @@ export default function AnnouncementProvider({ children }: Props) {
const { permission } = useLoggedInUser();
useEffect(() => {
if (announcements && announcements.length > 0) return;
if (permission?.dashboard_view === "blocked") return;
const initial = initialAnnouncements.map((announcement) => {
const hash = md5(announcement.text).toString();
@@ -51,12 +56,12 @@ export default function AnnouncementProvider({ children }: Props) {
...announcement,
hash,
isOpen,
};
} as AnnouncementInfo;
});
if (initial.length > 0) {
setAnnouncements(initial);
}
}, [closedAnnouncements]);
}, [closedAnnouncements, announcements]);
const closeAnnouncement = (hash: string) => {
setClosedAnnouncements([...closedAnnouncements, hash]);
@@ -81,7 +86,12 @@ export default function AnnouncementProvider({ children }: Props) {
return (
<AnnouncementContext.Provider
value={{ bannerHeight: height, announcements, closeAnnouncement }}
value={{
bannerHeight: height,
announcements,
closeAnnouncement,
setAnnouncements,
}}
>
{children}
</AnnouncementContext.Provider>

View File

@@ -17,11 +17,11 @@ const CountryContext = React.createContext(
);
export default function CountryProvider({ children }: Props) {
const { isUser } = useLoggedInUser();
const { permission } = useLoggedInUser();
const getRegionByPeer = (peer: Peer) => "Unknown";
return isUser ? (
return permission?.dashboard_view != "full" ? (
<CountryContext.Provider
value={{ countries: [], isLoading: false, getRegionByPeer }}
>
@@ -35,7 +35,7 @@ export default function CountryProvider({ children }: Props) {
function CountryProviderContent({ children }: Props) {
const { data: countries, isLoading } = useFetchApi<Country[]>(
"/locations/countries",
false,
true,
false,
);

View File

@@ -81,10 +81,13 @@ export default function DialogProvider({ children }: Props) {
/>
{dialogOptions.children && (
<div className={"px-8 pt-4"}>{dialogOptions.children}</div>
<div className={"px-8 pt-0"}>{dialogOptions.children}</div>
)}
<ModalFooter className={"items-center gap-2"} separator={false}>
<ModalFooter
className={"items-center gap-2 pt-5"}
separator={false}
>
<ModalClose asChild={true}>
<Button
variant={"secondary"}

View File

@@ -27,7 +27,7 @@ export default function UsersProvider({ children }: Props) {
return users?.find((user) => user.is_current);
}, [users]);
return !isLoading ? (
return !isLoading && loggedInUser ? (
<UsersContext.Provider value={{ users, refresh, loggedInUser }}>
{children}
</UsersContext.Provider>

View File

@@ -29,13 +29,13 @@ export default function DashboardLayout({
return (
<ApplicationProvider>
<UsersProvider>
<GroupsProvider>
<CountryProvider>
<AnnouncementProvider>
<AnnouncementProvider>
<GroupsProvider>
<CountryProvider>
<DashboardPageContent>{children}</DashboardPageContent>
</AnnouncementProvider>
</CountryProvider>
</GroupsProvider>
</CountryProvider>
</GroupsProvider>
</AnnouncementProvider>
</UsersProvider>
</ApplicationProvider>
);

View File

@@ -2,21 +2,21 @@
import { ScrollArea } from "@components/ScrollArea";
import { cn } from "@utils/helpers";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import { CustomFlowbiteTheme, Sidebar } from "flowbite-react";
import { SidebarItemGroupProps } from "flowbite-react/lib/esm/components/Sidebar/SidebarItemGroup";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import ActivityIcon from "@/assets/icons/ActivityIcon";
import DNSIcon from "@/assets/icons/DNSIcon";
import DocsIcon from "@/assets/icons/DocsIcon";
import IntegrationIcon from "@/assets/icons/IntegrationIcon";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import SettingsIcon from "@/assets/icons/SettingsIcon";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import TeamIcon from "@/assets/icons/TeamIcon";
import SidebarItem from "@/components/SidebarItem";
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { headerHeight } from "@/layouts/Header";
const customTheme: CustomFlowbiteTheme["sidebar"] = {
root: {
@@ -34,6 +34,7 @@ export default function Navigation({
hideOnMobile = false,
}: Props) {
const { isUser } = useLoggedInUser();
const { bannerHeight } = useAnnouncement();
return (
<Sidebar
@@ -42,123 +43,133 @@ export default function Navigation({
hideOnMobile ? "hidden md:block" : "",
fullWidth
? "w-auto max-w-[22rem]"
: "w-[15rem] min-w-[15rem] overflow-y-auto",
: "w-[15rem] max-w-[15rem] min-w-[15rem] overflow-y-auto",
)}
theme={customTheme}
style={{
height: fullWidth ? "calc(100vh - 75px)" : "100%",
height: fullWidth
? `calc(100vh - ${headerHeight + bannerHeight}px)`
: "100%",
}}
>
<Sidebar.Items className={cn(fullWidth ? "w-10/12" : "fixed")}>
<Sidebar.Items className={cn(fullWidth ? "w-10/12" : "fixed h-full")}>
<ScrollArea
style={{
height: !fullWidth ? "calc(100vh - 75px)" : "100%",
height: !fullWidth
? `calc(100vh - ${headerHeight + bannerHeight}px)`
: "100%",
}}
className={"pt-4"}
>
<SidebarItemGroup>
<SidebarItem icon={<PeerIcon />} label="Peers" href={"/peers"} />
{!isUser && (
<>
<div
className={
"flex flex-col justify-between pt-4 w-[15rem] max-w-[15rem] min-w-[15rem]"
}
style={{
height: !fullWidth
? `calc(100vh - ${headerHeight + bannerHeight}px)`
: "100%",
}}
>
<div>
<SidebarItemGroup>
<SidebarItem
icon={<SetupKeysIcon />}
label="Setup Keys"
href={"/setup-keys"}
icon={<PeerIcon />}
label="Peers"
href={"/peers"}
/>
<SidebarItem
icon={<AccessControlIcon />}
label="Access Control"
collapsible
>
{!isUser && (
<>
<SidebarItem
icon={<SetupKeysIcon />}
label="Setup Keys"
href={"/setup-keys"}
/>
<SidebarItem
icon={<AccessControlIcon />}
label="Access Control"
collapsible
>
<SidebarItem
label="Policies"
href={"/access-control"}
isChild
exactPathMatch={true}
/>
<SidebarItem
label="Posture Checks"
isChild
href={"/posture-checks"}
exactPathMatch={true}
/>
</SidebarItem>
<SidebarItem
icon={<NetworkRoutesIcon />}
label="Network Routes"
href={"/network-routes"}
/>
<SidebarItem
icon={<DNSIcon />}
label="DNS"
collapsible
exactPathMatch={true}
>
<SidebarItem
label="Nameservers"
isChild
href={"/dns/nameservers"}
/>
<SidebarItem
label="DNS Settings"
isChild
href={"/dns/settings"}
/>
</SidebarItem>
<SidebarItem icon={<TeamIcon />} label="Team" collapsible>
<SidebarItem label="Users" isChild href={"/team/users"} />
<SidebarItem
label="Service Users"
isChild
href={"/team/service-users"}
/>
</SidebarItem>
<SidebarItem
icon={<ActivityIcon />}
label="Activity"
href={"/activity"}
/>
</>
)}
{isUser && (
<SidebarItem
label="Policies"
href={"/access-control"}
isChild
icon={<DocsIcon />}
href={"https://docs.netbird.io/"}
target={"_blank"}
label="Documentation"
/>
)}
</SidebarItemGroup>
{!isUser && (
<SidebarItemGroup>
<SidebarItem
icon={<SettingsIcon />}
label="Settings"
href={"/settings"}
exactPathMatch={true}
/>
<SidebarItem
label="Posture Checks"
isChild
href={"/posture-checks"}
exactPathMatch={true}
/>
</SidebarItem>
<SidebarItem
icon={<NetworkRoutesIcon />}
label="Network Routes"
href={"/network-routes"}
/>
<SidebarItem
icon={<DNSIcon />}
label="DNS"
collapsible
exactPathMatch={true}
>
<SidebarItem
label="Nameservers"
isChild
href={"/dns/nameservers"}
icon={<DocsIcon />}
href={"https://docs.netbird.io/"}
target={"_blank"}
label="Documentation"
/>
<SidebarItem
label="DNS Settings"
isChild
href={"/dns/settings"}
/>
</SidebarItem>
<SidebarItem icon={<TeamIcon />} label="Team" collapsible>
<SidebarItem label="Users" isChild href={"/team/users"} />
<SidebarItem
label="Service Users"
isChild
href={"/team/service-users"}
/>
</SidebarItem>
<SidebarItem
icon={<ActivityIcon />}
label="Activity"
href={"/activity"}
/>
</>
)}
{isUser && (
<SidebarItem
icon={<DocsIcon />}
href={"https://docs.netbird.io/"}
target={"_blank"}
label="Documentation"
/>
)}
</SidebarItemGroup>
{!isUser && (
<SidebarItemGroup>
<SidebarItem
icon={<SettingsIcon />}
label="Settings"
href={"/settings"}
exactPathMatch={true}
/>
{(isLocalDev() || isNetBirdHosted()) && (
<SidebarItem
icon={<IntegrationIcon />}
label="Integrations"
href={"/integrations"}
exactPathMatch={true}
/>
</SidebarItemGroup>
)}
<SidebarItem
icon={<DocsIcon />}
href={"https://docs.netbird.io/"}
target={"_blank"}
label="Documentation"
/>
</SidebarItemGroup>
)}
</div>
</div>
</ScrollArea>
</Sidebar.Items>
</Sidebar>
@@ -167,7 +178,10 @@ export default function Navigation({
export function SidebarItemGroup(props: SidebarItemGroupProps) {
return (
<Sidebar.ItemGroup className={"dark:border-zinc-700/40"} {...props}>
<Sidebar.ItemGroup
className={"dark:border-zinc-700/40 space-y-1.5"}
{...props}
>
{props.children}
</Sidebar.ItemGroup>
);

View File

@@ -1,104 +0,0 @@
import Button from "@components/Button";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import { ToggleSwitch } from "@components/ToggleSwitch";
import { cn } from "@utils/helpers";
import { ExternalLinkIcon, Repeat } from "lucide-react";
import { StaticImport } from "next/dist/shared/lib/get-img-props";
import Image from "next/image";
import * as React from "react";
type Props<T> = {
image: StaticImport | string;
name: string;
description: string;
url: {
title: string;
href: string;
};
data?: T;
switchState: boolean;
onEnabledChange: (enabled: boolean) => void;
children?: React.ReactNode;
onSetup?: () => void;
disabled?: boolean;
hideSwitch?: boolean;
};
export function IntegrationCard<T>({
image,
name,
description,
url,
data,
switchState,
onEnabledChange,
children,
onSetup,
disabled,
hideSwitch = false,
}: Props<T>) {
return (
<div
className={cn(
" border border-nb-gray-900/50 p-5 rounded-lg transition-all max-w-[360px] flex flex-col justify-between gap-4",
switchState ? "bg-nb-gray-930/50" : "bg-nb-gray-930/30",
disabled && "opacity-60 pointer-events-none",
)}
>
<div className={"flex flex-col gap-4"}>
<div className={"flex justify-between"}>
<div className={"flex gap-4"}>
<div
className={
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
}
>
<Image src={image} alt={name} className={"rounded-[4px]"} />
</div>
<div>
<h3 className={""}>{name}</h3>
<InlineLink
href={url.href}
target={"_blank"}
className={"text-sm font-light"}
variant={"faded"}
>
{url.title}
<ExternalLinkIcon size={12} />
</InlineLink>
</div>
</div>
{!hideSwitch && (
<div className={"flex items-center"}>
<ToggleSwitch
checked={switchState}
onCheckedChange={onEnabledChange}
className={"grow"}
/>
</div>
)}
</div>
<div>
<Paragraph className={"text-sm font-light"}>{description}</Paragraph>
</div>
</div>
{data == undefined ? (
<div>
<Button
variant={"secondary"}
size={"xs"}
className={"w-full items-center"}
onClick={onSetup}
>
<Repeat size={13} />
Connect {name}
</Button>
</div>
) : (
children
)}
</div>
);
}

View File

@@ -1,56 +0,0 @@
import Paragraph from "@components/Paragraph";
import { cn } from "@utils/helpers";
import { ArrowRightLeft } from "lucide-react";
import { StaticImport } from "next/dist/shared/lib/get-img-props";
import Image from "next/image";
import * as React from "react";
import netBirdLogo from "@/assets/netbird.svg";
type Props = {
image: StaticImport | string;
title: string;
description: string;
};
export const IntegrationModalHeader = ({
image,
title,
description,
}: Props) => {
return (
<>
<div className={"flex justify-center items-center gap-4 mt-5"}>
<div
className={
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
}
>
<Image
src={netBirdLogo}
alt={"NetBird"}
className={"rounded-[4px]"}
/>
</div>
<div>
<ArrowRightLeft size={24} className={"text-netbird"} />
</div>
<div
className={
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
}
>
<Image src={image} alt={""} className={"rounded-[4px]"} />
</div>
</div>
<div
className={
"mx-auto text-center flex flex-col items-center justify-center mt-6 z-[1]"
}
>
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>{title}</h2>
<Paragraph className={cn("text-sm text-center max-w-[450px] px-4")}>
{description}
</Paragraph>
</div>
</>
);
};

View File

@@ -1,70 +0,0 @@
import { IconCircleFilled } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { FileText } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import * as React from "react";
import datadogLogo from "@/assets/integrations/datadog.png";
import { EventStream } from "@/interfaces/EventStream";
export const EventStreamingCard = () => {
const { data: eventStreamIntegrations } = useFetchApi<EventStream[]>(
"/integrations/event-streaming",
);
const dataDogSettings = eventStreamIntegrations?.find(
(integration) => integration.platform === "datadog",
);
const enabled = dataDogSettings ? dataDogSettings.enabled : false;
const router = useRouter();
return (
<div className={"p-default pb-6"}>
<div
onClick={() => router.push("/integrations")}
className={cn(
"border cursor-pointer border-nb-gray-900/50 bg-nb-gray-900/30 hover:bg-nb-gray-900/50 py-3 pl-3 pr-5 rounded-lg transition-all min-w-[310px] max-w-[400px]",
)}
>
<div className={"inline-flex gap-4 w-full"}>
<div
className={
"h-10 w-10 shrink-0 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70"
}
>
{dataDogSettings?.enabled && (
<Image
src={datadogLogo}
alt={"Datadog"}
className={"rounded-[4px]"}
/>
)}
{!dataDogSettings && <FileText size={16} />}
</div>
<div className={""}>
<div className={"flex items-center gap-3 justify-between"}>
<div className={"font-medium text-sm flex gap-2 items-center"}>
Event Streaming
</div>
<div
className={cn(
"text-xs flex gap-2 items-center mb-2 font-medium",
enabled ? "text-green-500" : "text-nb-gray-500",
)}
>
<IconCircleFilled size={8} />
{enabled ? "Enabled" : "Disabled"}
</div>
</div>
<p className={"text-xs font-light !text-nb-gray-300 "}>
Stream your activity events to third-party services.
</p>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,49 +0,0 @@
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import * as Tabs from "@radix-ui/react-tabs";
import { ExternalLinkIcon, FileText } from "lucide-react";
import React from "react";
import IntegrationIcon from "@/assets/icons/IntegrationIcon";
import Datadog from "@/modules/integrations/event-streaming/datadog/Datadog";
export default function EventStreamingTab() {
return (
<Tabs.Content value={"event-streaming"}>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/integrations"}
label={"Integrations"}
icon={<IntegrationIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/integrations"}
label={"Event Streaming"}
icon={<FileText size={14} />}
active
/>
</Breadcrumbs>
<h1>Event Streaming</h1>
<Paragraph>
Event Streaming allows you to stream NetBirds activity events to
different third-party services.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/activity-event-streaming"}
target={"_blank"}
>
Event Streaming
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
<div className={"gap-6 mt-6 flex flex-wrap"}>
<Datadog />
</div>
</div>
</Tabs.Content>
);
}

View File

@@ -1,75 +0,0 @@
import { notify } from "@components/Notification";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import useFetchApi, { useApiCall } from "@utils/api";
import * as React from "react";
import { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/datadog.png";
import { useDialog } from "@/contexts/DialogProvider";
import { EventStream } from "@/interfaces/EventStream";
import DatadogSetup from "@/modules/integrations/event-streaming/datadog/DatadogSetup";
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
export default function Datadog() {
const { mutate } = useSWRConfig();
const { data: eventStreamIntegrations, isLoading } = useFetchApi<
EventStream[]
>("/integrations/event-streaming");
const dataDogSettings = eventStreamIntegrations?.find(
(integration) => integration.platform === "datadog",
);
const integrationRequest = useApiCall<EventStream>(
"/integrations/event-streaming",
);
const [setupModal, setSetupModal] = useState(false);
const { confirm } = useDialog();
const toggleSwitch = async () => {
if (!dataDogSettings) return setSetupModal(true);
const choice = await confirm({
title: `Disconnect Datadog?`,
description:
"Disconnecting deletes the current configuration. You will need to start the setup process again.",
confirmText: "Disconnect",
cancelText: "Cancel",
type: "warning",
});
if (!choice) return;
notify({
title: "Datadog Integration",
description: `Datadog was successfully disconnected`,
promise: integrationRequest.del({}, "/" + dataDogSettings.id).then(() => {
mutate("/integrations/event-streaming");
}),
loadingMessage: "Disconnecting integration...",
});
};
return isLoading ? (
<>
<SkeletonIntegration />
</>
) : (
<>
<IntegrationCard
name="Datadog"
description="Datadog is a monitoring service for cloud-scale applications."
url={{
title: "datadoghq.com",
href: "https://www.datadoghq.com/",
}}
image={integrationImage}
data={dataDogSettings}
switchState={!dataDogSettings ? false : dataDogSettings.enabled}
onEnabledChange={toggleSwitch}
onSetup={() => setSetupModal(true)}
></IntegrationCard>
<DatadogSetup open={setupModal} onOpenChange={setSetupModal} />
</>
);
}

View File

@@ -1,44 +0,0 @@
import { CountryEURounded } from "@/assets/countries/CountryEURounded";
import { CountryJPRounded } from "@/assets/countries/CountryJPRounded";
import { CountryUSRounded } from "@/assets/countries/CountryUSRounded";
export const DatadogRegions = [
{
name: "Europe (EU)",
site_url: "https://app.datadoghq.eu",
send_logs_url: "https://http-intake.logs.datadoghq.eu/api/v2/logs",
icon: CountryEURounded,
},
{
name: "United States (US1)",
site_url: "https://app.datadoghq.com",
send_logs_url: "https://http-intake.logs.datadoghq.com/api/v2/logs",
icon: CountryUSRounded,
},
{
name: "United States (US3)",
site_url: "https://us3.datadoghq.com",
send_logs_url: "https://http-intake.logs.us3.datadoghq.com/api/v2/logs",
icon: CountryUSRounded,
},
{
name: "United States (US5)",
site_url: "https://us5.datadoghq.com",
send_logs_url: "https://http-intake.logs.us5.datadoghq.com/api/v2/logs",
icon: CountryUSRounded,
},
{
name: "United States (US1-FED)",
site_url: "https://app.ddog-gov.com",
send_logs_url: "https://http-intake.logs.ddog-gov.com/api/v2/logs",
icon: CountryUSRounded,
},
{
name: "Japan (AP1)",
site_url: "https://ap1.datadoghq.com",
send_logs_url: "https://http-intake.logs.ap1.datadoghq.com/api/v2/logs",
icon: CountryJPRounded,
},
] as const;
export const DatadogApiKeysPage = "/organization-settings/api-keys";

View File

@@ -1,277 +0,0 @@
import Button from "@components/Button";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
import { notify } from "@components/Notification";
import {
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import Steps from "@components/Steps";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import {
ExternalLinkIcon,
Globe,
GlobeIcon,
KeyRound,
Repeat,
} from "lucide-react";
import Link from "next/link";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import datadogLogo from "@/assets/integrations/datadog.png";
import { EventStream } from "@/interfaces/EventStream";
import {
DatadogApiKeysPage,
DatadogRegions,
} from "@/modules/integrations/event-streaming/datadog/DatadogRegions";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function DatadogSetup({ open, onOpenChange, onSuccess }: Props) {
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
<SetupContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
/>
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
};
export function SetupContent({ onSuccess }: ModalProps) {
const { mutate } = useSWRConfig();
const integrationRequest = useApiCall<EventStream>(
"/integrations/event-streaming",
);
const datadogRegions = DatadogRegions.map((region) => {
return {
label: region.name,
value: region.send_logs_url,
icon: region.icon,
} as SelectOption;
});
const [selectedRegion, setSelectedRegion] = useState(datadogRegions[0].value);
const changeRegion = (region: string) => {
setSelectedRegion(region);
setApiUrl(region);
};
const [apiKey, setApiKey] = useState("");
const [apiUrl, setApiUrl] = useState(datadogRegions[0].value);
const [step, setStep] = useState(1);
const apiKeyEntered = apiKey.length > 0 && apiKey != "";
const apiUrlEntered = apiUrl.length > 0 && apiUrl != "";
const apiKeyAndUrlEntered = apiKeyEntered && apiUrlEntered;
const apiPageUrl =
DatadogRegions.find((region) => region.send_logs_url == apiUrl)?.site_url +
DatadogApiKeysPage;
const connect = async () => {
notify({
title: "Datadog Integration",
description: `Datadog was successfully connected to NetBird.`,
promise: integrationRequest
.post({
platform: "datadog",
config: {
api_key: apiKey,
api_url: apiUrl,
},
enabled: true,
})
.then(() => {
mutate("/integrations/event-streaming");
onSuccess();
}),
loadingMessage: "Setting up integration...",
});
};
return (
<ModalContent
maxWidthClass={cn("relative", step === 1 ? "max-w-md" : "max-w-lg")}
showClose={true}
>
<GradientFadedBackground />
<IntegrationModalHeader
image={datadogLogo}
title={"Connect NetBird with Datadog"}
description={
"Start streaming your NetBird activity events to Datadog. Follow the steps below to get started."
}
/>
{step == 1 && (
<div className={"px-8 py-3 flex flex-col mt-4 z-0"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<GlobeIcon size={16} />
Select your Datadog region
</p>
<p className={"mb-3 mt-2"}>
To identify which region you are on please check out the{" "}
<InlineLink
href={"https://docs.datadoghq.com/getting_started/site/"}
target={"_blank"}
variant={"default"}
className={"inline"}
>
Datadog Documentation.
</InlineLink>
</p>
<SelectDropdown
value={selectedRegion}
onChange={changeRegion}
options={datadogRegions}
/>
<div className={"mt-3 hidden"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"flex items-center gap-2"}>
<Globe size={16} className={"text-nb-gray-300"} />
</div>
}
placeholder={"https://http-intake.logs.datadoghq.eu/api/v2/logs"}
value={apiUrl}
onChange={(e) => setApiUrl(e.target.value)}
/>
</div>
<div className={"mb-3"}></div>
</div>
)}
{step == 2 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4 z-0"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<KeyRound size={16} />
Get your Datadog API Key
</p>
<Steps>
<Steps.Step step={1}>
<p>Navigate to Datadogs API Keys page</p>
<div className={"flex gap-4"}>
<Link href={apiPageUrl} passHref target={"_blank"}>
<Button variant={"primary"} size={"xs"}>
<ExternalLinkIcon size={14} />
API Keys
</Button>
</Link>
</div>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click{" "}
<div
className={
"inline-flex bg-nb-gray-900 py-1.5 px-2.5 rounded-md text-xs items-center mx-0.5"
}
>
+ New Key
</div>{" "}
at the top
</p>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Give it a descriptive name like{" "}
<div
className={
"inline-flex bg-nb-gray-900 py-1.5 px-2.5 rounded-md text-xs items-center mx-0.5"
}
>
NetBird Activity Events
</div>
and click{" "}
<div
className={
"inline-flex bg-nb-gray-900 py-1.5 px-2.5 rounded-md text-xs items-center mx-0.5"
}
>
Create Key
</div>
</p>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p className={"font-normal"}>Enter your API-Key</p>
</Steps.Step>
</Steps>
<div className={"mb-4"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"flex items-center gap-2"}>
<KeyRound size={16} className={"text-nb-gray-300"} />
</div>
}
placeholder={"1c17401cf170f7ac33dd9dcdf8040eb2"}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
</div>
)}
<ModalFooter className={"items-center gap-4"}>
{step == 1 && (
<Button
variant={"primary"}
className={"w-full"}
disabled={!apiUrlEntered}
onClick={() => setStep(2)}
>
Continue
<IconArrowRight size={16} />
</Button>
)}
{step == 2 && (
<>
<Button
variant={"secondary"}
className={"w-full"}
onClick={() => setStep(1)}
>
<IconArrowLeft size={16} />
Back
</Button>
<Button
variant={"primary"}
className={"w-full"}
disabled={!apiKeyAndUrlEntered}
onClick={connect}
>
<Repeat size={16} />
Connect
</Button>
</>
)}
</ModalFooter>
</ModalContent>
);
}

View File

@@ -1,109 +0,0 @@
import Button from "@components/Button";
import { Input } from "@components/Input";
import { useDebounce } from "@hooks/useDebounce";
import { Folder, MinusCircleIcon, PlusIcon } from "lucide-react";
import * as React from "react";
import { useEffect, useState } from "react";
type GroupPrefixInputProps = {
value: string[];
onChange: (values: string[]) => void;
addText?: string;
icon?: React.ReactNode;
text?: string;
placeholder?: string;
};
export function GroupPrefixInput({
value,
onChange,
addText = "Add group filter",
icon = <Folder size={14} />,
text = "Group starts with...",
placeholder = "e.g., NetBird_",
}: GroupPrefixInputProps) {
const [groupPrefixes, setGroupPrefixes] = useState<string[]>(value);
const prefixes = useDebounce(groupPrefixes, 100);
useEffect(() => {
onChange(prefixes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [prefixes]);
const onChangeHandler = (
e: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const newPrefixes = [...groupPrefixes];
newPrefixes[index] = e.target.value;
setGroupPrefixes(newPrefixes);
};
const onRemoveGroupPrefix = (index: number) => {
setGroupPrefixes((p) => {
const newPrefixes = [...p];
newPrefixes.splice(index, 1);
return newPrefixes;
});
};
const onAddGroupPrefix = () => {
setGroupPrefixes((p) => {
const newPrefixes = [...p];
newPrefixes.push("");
return newPrefixes;
});
};
return (
<div className={"mt-4"}>
{groupPrefixes.length > 0 && (
<div className={"flex gap-3 w-full mb-3"}>
<div className={"flex flex-col gap-2 w-full"}>
{groupPrefixes.map((g, i) => {
return (
<div className={"flex gap-2 w-full"} key={i}>
<div className={"w-full"}>
<Input
customPrefix={
<div className={"flex gap-2 items-center"}>
{icon}
<span>{text}</span>
</div>
}
placeholder={placeholder}
maxWidthClass={"w-full"}
value={g}
className={" !text-[13px]"}
onKeyDown={(event) => {
if (event.code === "Space") event.preventDefault();
}}
onChange={(e) => onChangeHandler(e, i)}
/>
</div>
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => onRemoveGroupPrefix(i)}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
</div>
)}
<Button
variant={"dotted"}
className={"w-full"}
size={"sm"}
onClick={onAddGroupPrefix}
>
<PlusIcon size={14} />
{addText}
</Button>
</div>
);
}

View File

@@ -1,92 +0,0 @@
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import { Label } from "@components/Label";
import Paragraph from "@components/Paragraph";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import * as Tabs from "@radix-ui/react-tabs";
import { ExternalLinkIcon, FingerprintIcon } from "lucide-react";
import React from "react";
import IntegrationIcon from "@/assets/icons/IntegrationIcon";
import { useAccount } from "@/modules/account/useAccount";
import { AzureAD } from "@/modules/integrations/idp-sync/azure-ad/AzureAD";
import { GoogleWorkspace } from "@/modules/integrations/idp-sync/google-workspace/GoogleWorkspace";
import { Okta } from "@/modules/integrations/idp-sync/okta-scim/Okta";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
export default function IdentityProviderTab() {
const account = useAccount();
useIntegrations();
return (
<Tabs.Content value={"identity-provider"}>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/integrations"}
label={"Integrations"}
icon={<IntegrationIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/settings"}
label={"Identity Provider"}
icon={<FingerprintIcon size={14} />}
active
/>
</Breadcrumbs>
<h1>Identity Provider</h1>
<Paragraph>
Configure your preferred Identity Provider (IdP) to synchronize your
users and groups to NetBird.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/idp-sync"}
target={"_blank"}
>
Identity Provider
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
<div className={"gap-6 mt-6 flex flex-wrap"}>
{!account ? (
<>
<SkeletonIntegration loadingHeight={196} />
<SkeletonIntegration loadingHeight={196} />
<SkeletonIntegration loadingHeight={196} />
</>
) : (
<>
<GoogleWorkspace />
<AzureAD />
<Okta />
</>
)}
</div>
<div className={"flex flex-col gap-6 max-w-lg mt-10"}>
<div
className={
"bg-netbird-950 px-6 py-4 rounded-md border border-netbird-500 "
}
>
<Label className={"!text-netbird-100 text-md"}>
Looking to enable a custom IDP like Jumpcloud?
</Label>
<p className={"!text-netbird-200 mt-2"}>
Please contact us at{" "}
<InlineLink
href={"mailto:support@netbird.io"}
className={"inline !text-netbird-500 font-medium"}
>
{" "}
support@netbird.io
</InlineLink>{" "}
</p>
</div>
</div>
</div>
</Tabs.Content>
);
}

View File

@@ -1,164 +0,0 @@
import Button from "@components/Button";
import FullTooltip from "@components/FullTooltip";
import { notify } from "@components/Notification";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import useFetchApi, { useApiCall } from "@utils/api";
import dayjs from "dayjs";
import { isEmpty } from "lodash";
import { RefreshCw, Settings } from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/entra-id.png";
import {
AzureADIntegration,
IdentityProviderLog,
} from "@/interfaces/IdentityProvider";
import AzureADConfiguration from "@/modules/integrations/idp-sync/azure-ad/AzureADConfiguration";
import AzureADSetup from "@/modules/integrations/idp-sync/azure-ad/AzureADSetup";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
export const AzureAD = () => {
const { mutate } = useSWRConfig();
const [setupModal, setSetupModal] = useState(false);
const {
azure: integration,
isAnyIntegrationEnabled,
isAzureLoading,
} = useIntegrations();
const azureRequest = useApiCall<AzureADIntegration>(
"/integrations/azure-idp",
);
const [enabled, setEnabled] = useState(
integration ? integration.enabled : false,
);
useEffect(() => {
setEnabled(integration?.enabled ?? false);
}, [integration]);
const toggleSwitch = async (state: boolean) => {
if (!integration) return setSetupModal(true);
notify({
title: "Entra ID (Azure AD) Integration",
description: `Entra ID (Azure AD) was successfully ${
state ? "enabled" : "disabled"
}`,
promise: azureRequest
.put(
{
enabled: state,
},
"/" + integration.id,
)
.then(() => {
mutate("/integrations/azure-idp");
setEnabled(state);
}),
loadingMessage: "Updating integration...",
});
};
return isAzureLoading ? (
<SkeletonIntegration loadingHeight={197} />
) : (
<>
<IntegrationCard
name="Entra ID (Azure AD)"
description="Microsoft Entra ID is a cloud-based identity and access management solution."
url={{
title: "microsoft.com",
href: "https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id",
}}
image={integrationImage}
data={integration}
disabled={enabled ? false : isAnyIntegrationEnabled}
switchState={enabled}
onEnabledChange={toggleSwitch}
onSetup={() => setSetupModal(true)}
>
{integration && <ConfigurationButton config={integration} />}
</IntegrationCard>
<AzureADSetup
open={setupModal}
onOpenChange={setSetupModal}
onSuccess={() => setEnabled(true)}
/>
</>
);
};
type ConfigurationProps = {
config: AzureADIntegration;
};
const ConfigurationButton = ({ config }: ConfigurationProps) => {
const { data: logs } = useFetchApi<IdentityProviderLog[]>(
`/integrations/azure-idp/${config.id}/logs`,
);
const { mutate } = useSWRConfig();
const syncRequest = useApiCall<{ response: boolean }>(
`/integrations/azure-idp/${config.id}/sync`,
);
const [configModal, setConfigModal] = useState(false);
const forceSync = async () => {
notify({
title: "Entra ID (Azure AD) Integration",
description: `Entra ID (Azure AD) was successfully synced`,
loadingMessage: "Syncing integration...",
promise: syncRequest.post({}).then(() => {
mutate(`/integrations/azure-idp/${config.id}/logs`);
}),
});
};
const lastSync = useMemo(() => {
if (isEmpty(logs)) return "Not synchronized";
return "Synced " + dayjs().to(logs?.[0]?.timestamp);
}, [logs]);
return (
<>
<div className={"flex gap-2"}>
<FullTooltip
content={
<div className={"text-xs"}>
Force synchronization of users and groups
</div>
}
disabled={!config.enabled}
className={"w-full"}
interactive={false}
>
<Button
variant={"secondary"}
size={"xs"}
className={"w-full items-center"}
onClick={forceSync}
disabled={!config.enabled}
>
<RefreshCw size={14} />
{lastSync}
</Button>
</FullTooltip>
<Button
variant={"secondary"}
size={"xs"}
className={"items-center"}
onClick={() => {
setConfigModal(true);
}}
>
<Settings size={14} />
</Button>
</div>
<AzureADConfiguration open={configModal} onOpenChange={setConfigModal} />
</>
);
};

View File

@@ -1,372 +0,0 @@
import Button from "@components/Button";
import HelpText from "@components/HelpText";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
} from "@components/modal/Modal";
import { notify } from "@components/Notification";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { useHasChanges } from "@hooks/useHasChanges";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import {
AlertOctagon,
Box,
Cog,
Folder,
FolderGit2,
KeyRound,
RefreshCw,
UserCircle,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/entra-id.png";
import { useDialog } from "@/contexts/DialogProvider";
import { AzureADIntegration } from "@/interfaces/IdentityProvider";
import { GroupPrefixInput } from "@/modules/integrations/idp-sync/GroupPrefixInput";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function AzureADConfiguration({
open,
onOpenChange,
onSuccess,
}: Props) {
const { azure } = useIntegrations();
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
{azure && (
<ConfigurationContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
config={azure}
/>
)}
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
config: AzureADIntegration;
};
export function ConfigurationContent({ onSuccess, config }: ModalProps) {
const { mutate } = useSWRConfig();
const { confirm } = useDialog();
const [tab, setTab] = useState<string>("settings");
const azureRequest = useApiCall<AzureADIntegration>(
"/integrations/azure-idp",
);
const clientSecretPlaceholder = "******************************";
const [clientSecret, setClientSecret] = useState(clientSecretPlaceholder);
const [clientId, setClientId] = useState(config.clientId);
const [tenantId, setTenantId] = useState(config.tenantId);
const [interval, setInterval] = useState(config.syncInterval.toString());
const [groupPrefixes, setGroupPrefixes] = useState<string[]>(
config.group_prefixes || [],
);
const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>(
config.user_group_prefixes || [],
);
const deleteIntegration = async () => {
const choice = await confirm({
title: `Delete integration?`,
description: "Are you sure you want to delete this integration?",
confirmText: "Delete",
cancelText: "Cancel",
type: "danger",
});
if (!choice) return;
notify({
title: "Entra ID (Azure AD) Integration",
description: `Entra ID (Azure AD) was successfully deleted`,
promise: azureRequest.del({}, `/${config.id}`).then(() => {
mutate("/integrations/azure-idp");
onSuccess();
}),
loadingMessage: "Deleting integration...",
});
};
const updateIntegration = async () => {
notify({
title: "Entra ID (Azure AD) Integration",
description: `Entra ID (Azure AD) was successfully updated`,
promise: azureRequest
.put(
{
client_id: clientId,
tenant_id: tenantId,
client_secret:
clientSecretPlaceholder == clientSecret
? undefined
: btoa(clientSecret),
sync_interval: interval ? parseInt(interval) : 300,
group_prefixes: groupPrefixes || [],
user_group_prefixes: userGroupPrefixes || [],
},
`/${config.id}`,
)
.then(() => {
mutate("/integrations/azure-idp");
onSuccess();
}),
loadingMessage: "Updating integration...",
});
};
const { hasChanges } = useHasChanges([
clientId,
tenantId,
clientSecret,
interval,
groupPrefixes,
userGroupPrefixes,
]);
return (
<ModalContent
maxWidthClass={cn("relative max-w-xl")}
showClose={true}
className={""}
autoFocus={false}
>
<GradientFadedBackground />
<IntegrationModalHeader
image={integrationImage}
title={"Entra ID (Azure AD) Configuration"}
description={"Sync your users and groups from Entra ID to NetBird."}
/>
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
className={"mt-6"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"settings"}>
<Cog
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Settings
</TabsTrigger>
<TabsTrigger value={"group-sync"}>
<FolderGit2
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Group Sync
</TabsTrigger>
<TabsTrigger value={"user-sync"}>
<UserCircle
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
User Sync
</TabsTrigger>
<TabsTrigger value={"danger"}>
<AlertOctagon
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Danger Zone
</TabsTrigger>
</TabsList>
<TabsContent value={"settings"} className={"px-8 text-sm"}>
<div className={"flex-col gap-3 flex"}>
<Input
type={"text"}
autoCorrect={"off"}
autoComplete={"off"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<Box size={16} />
Application (client) ID
</div>
}
placeholder={"62d3a656-c87d-4f30-a242-5b6347e29e9f"}
value={clientId}
onChange={(e) => setClientId(e.target.value)}
/>
<Input
autoCorrect={"off"}
autoComplete={"off"}
type={"text"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<Folder size={16} />
Directory (tenant) ID
</div>
}
placeholder={"5d60468a-65b7-45eb-a61a-53ecfbcd1ea3"}
value={tenantId}
onChange={(e) => setTenantId(e.target.value)}
/>
<Input
autoCorrect={"off"}
type={"text"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<KeyRound size={16} />
Client Secret
</div>
}
placeholder={"YdV7Q~JJ62Xl.LvYoBanxZR2sJA2va_3UbqvncY8"}
value={clientSecret}
onChange={(e) => setClientSecret(e.target.value)}
/>
<div className={"flex justify-between mt-4"}>
<div>
<Label>Sync Interval</Label>
<HelpText className={"max-w-[300px]"}>
The interval in seconds when the synchronization should
happen.
</HelpText>
</div>
<Input
maxWidthClass={"max-w-[400px]"}
placeholder={"300"}
min={1}
max={99999}
value={interval}
type={"number"}
onChange={(e) => setInterval(e.target.value)}
customPrefix={
<RefreshCw size={16} className={"text-nb-gray-300"} />
}
customSuffix={"Seconds"}
/>
</div>
</div>
</TabsContent>
<TabsContent value={"group-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<FolderGit2 size={16} />
Synchronize Groups
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Groups</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only groups that start with a specific
prefix, you can add them below.
</HelpText>
</div>
<GroupPrefixInput value={groupPrefixes} onChange={setGroupPrefixes} />
</TabsContent>
<TabsContent value={"user-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<UserCircle size={16} />
Synchronize Users
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Users</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only users that belong to a specific
group, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
addText={"Add user group filter"}
text={"User group starts with..."}
value={userGroupPrefixes}
onChange={setUserGroupPrefixes}
/>
</TabsContent>
<TabsContent value={"danger"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<AlertOctagon size={16} />
Delete Integration
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
Deleting this integration will remove the ability to sync users
and groups from your IdP to NetBird. If you delete the integration
you will need to reconfigure it again to enable the
synchronization.
</HelpText>
</div>
<Button
variant={"danger"}
size={"xs"}
className={"mt-3"}
onClick={deleteIntegration}
>
Delete Integration
</Button>
</TabsContent>
</Tabs>
<div className={"h-6"}></div>
<ModalFooter className={"items-center gap-4"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
Cancel
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
disabled={!hasChanges}
onClick={updateIntegration}
>
Save
</Button>
</ModalFooter>
</ModalContent>
);
}

View File

@@ -1,498 +0,0 @@
import Button from "@components/Button";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
import { notify } from "@components/Notification";
import Steps from "@components/Steps";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { Lightbox } from "@components/ui/Lightbox";
import { Mark } from "@components/ui/Mark";
import { MinimalList } from "@components/ui/MinimalList";
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import { isEmpty } from "lodash";
import {
Box,
Clock4,
Folder,
FolderGit2,
KeyRound,
PlusCircle,
Repeat,
Settings2,
Shield,
UserCircle,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/entra-id.png";
import { AzureADIntegration } from "@/interfaces/IdentityProvider";
import azureGrantAdmin from "@/modules/integrations/idp-sync/azure-ad/images/azure-grant-admin-conset.png";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
import { GroupPrefixInput } from "../GroupPrefixInput";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function AzureADSetup({ open, onOpenChange, onSuccess }: Props) {
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
<SetupContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
/>
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
};
export function SetupContent({ onSuccess }: ModalProps) {
const { mutate } = useSWRConfig();
const azureRequest = useApiCall<AzureADIntegration>(
"/integrations/azure-idp",
);
const [step, setStep] = useState(0);
const maxSteps = 6;
const [clientSecret, setClientSecret] = useState("");
const [clientId, setClientId] = useState("");
const [tenantId, setTenantId] = useState("");
const clientSecretEntered = !isEmpty(clientSecret);
const clientIdEntered = !isEmpty(clientId);
const tenantIdEntered = !isEmpty(tenantId);
const allEntered = clientIdEntered && tenantIdEntered && clientSecretEntered;
const isDisabled =
(step == 8 && !clientSecretEntered) || (step == 9 && !allEntered);
const [groupPrefixes, setGroupPrefixes] = useState<string[]>([]);
const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>([]);
const connect = async () => {
notify({
title: "Entra ID Integration",
description: `Entra ID was successfully connected to NetBird.`,
promise: azureRequest
.post({
client_secret: btoa(clientSecret), // Encode client secret to base64
client_id: clientId,
tenant_id: tenantId,
group_prefixes: groupPrefixes || [],
user_group_prefixes: userGroupPrefixes || [],
})
.then(() => {
mutate("/integrations/azure-idp");
onSuccess();
}),
loadingMessage: "Setting up integration...",
});
};
return (
<ModalContent
maxWidthClass={cn("relative", step == 0 ? "max-w-md" : "max-w-xl")}
showClose={true}
className={""}
onEscapeKeyDown={(e) => step > 0 && e.preventDefault()}
onInteractOutside={(e) => step > 0 && e.preventDefault()}
onPointerDownOutside={(e) => step > 0 && e.preventDefault()}
>
<GradientFadedBackground />
{step > 0 && (
<div className={"flex gap-2 w-full items-center justify-center mb-4"}>
{Array.from({ length: maxSteps }).map((_, index) => (
<div
key={index}
className={cn(
"w-8 h-1 rounded-full bg-nb-gray-800",
step >= index + 1 && "bg-netbird",
)}
/>
))}
</div>
)}
<IntegrationModalHeader
image={integrationImage}
title={"Connect NetBird with Entra ID (Azure AD)"}
description={
"Start syncing your users and groups from Entra ID to NetBird. Follow the steps below to get started."
}
/>
{step == 0 && (
<div
className={
"px-8 py-3 flex z-0 flex-col gap-0 text-sm mb-3 text-center justify-center items-center"
}
>
<div
className={
"mt-6 text-base font-medium text-nb-gray-100 flex gap-2 items-center justify-center"
}
>
<Shield size={16} />
Required Permissions
</div>
<p className={"mt-2 !text-nb-gray-300 !leading-[1.5]"}>
Ensure that you have an an{" "}
<span className={"text-nb-gray-100 font-semibold"}>
Azure AD user account
</span>{" "}
with the following{" "}
<span className={"text-nb-gray-100 font-semibold"}>
permissions
</span>
.{" "}
{
"If you don't have the required permissions, ask your Azure AD administrator to grant them to you."
}
</p>
<div
className={
"flex items-center flex-col gap-0 mt-2 w-full justify-center max-w-lg"
}
>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<PlusCircle size={14} className={"text-sky-500"} />
Create Azure AD applications
</div>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<Settings2 size={14} className={"text-sky-500"} />
Manage Azure AD applications
</div>
</div>
</div>
)}
{step == 1 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Create and configure Azure AD application
</p>
<Steps>
<Steps.Step step={1}>
<p>
Navigate to{" "}
<InlineLink
className={"inline"}
target={"_blank"}
href={
"https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview"
}
>
Azure Active Directory
</InlineLink>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>App Registrations</Mark> in the left menu then click
on the <Mark>+ New registration</Mark> button to create a new
application.
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Fill in the form with the following values and click{" "}
<Mark>Register</Mark>
</p>
</Steps.Step>
</Steps>
<MinimalList
data={[
{
label: "Name",
value: "NetBird",
},
{
label: "Account Types",
value:
"Accounts in this organizational directory only (Default Directory only - Single tenant)",
},
{
label: "Redirect Type",
value: "Single-page application (SPA)",
},
{
label: "Redirect URI",
value: "https://app.netbird.io/silent-auth",
},
]}
/>
</div>
)}
{step == 2 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Shield size={20} />
Add API permissions
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Click <Mark>API permissions</Mark> on the left side menu
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>Add a permission</Mark> then{" "}
<Mark>Microsoft Graph</Mark> and then on the{" "}
<Mark>Application permissions</Mark> tab.
</p>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
In <Mark>Select permissions</Mark> select{" "}
<Mark>User.Read.All</Mark> and <Mark>Group.Read.All</Mark> and
click <Mark>Add permissions</Mark>
</p>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p className={"font-normal"}>
Click <Mark>Grant admin conset for Default Directory</Mark> and
click <Mark>Yes</Mark>
</p>
<Lightbox image={azureGrantAdmin} />
</Steps.Step>
</Steps>
</div>
)}
{step == 3 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<KeyRound size={20} />
Generate client secret
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Navigate to <Mark>Certificates & secrets</Mark> on left side
menu
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click on <Mark>+ New client secret</Mark>
</p>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Add <Mark copy>NetBird</Mark> as the description and click{" "}
<Mark>Add</Mark>
</p>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p className={"font-normal"}>
Copy the <Mark>Value</Mark> and paste it here
</p>
</Steps.Step>
</Steps>
<div className={"mb-4"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"flex items-center gap-2"}>
<KeyRound size={16} className={"text-nb-gray-300"} />
</div>
}
placeholder={"YdV7Q~JJ62Xl.LvYoBanxZR2sJA2va_3UbqvncY8"}
value={clientSecret}
onChange={(e) => setClientSecret(e.target.value)}
/>
</div>
</div>
)}
{step == 4 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Enter Application ID and Directory ID
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Navigate to{" "}
<InlineLink
target={"_blank"}
className={"inline"}
href={
"https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps"
}
>
Owner applications
</InlineLink>
</p>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Select <Mark>NetBird</Mark> application in overview page and
enter your <Mark>Application (client) ID</Mark> and{" "}
<Mark>Directory (tenant) ID</Mark>
</p>
</Steps.Step>
</Steps>
<div className={"mb-4 flex flex-col gap-3"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<Box size={16} />
Application (client) ID
</div>
}
placeholder={"62d3a656-c87d-4f30-a242-5b6347e29e9f"}
value={clientId}
onChange={(e) => setClientId(e.target.value)}
/>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<Folder size={16} />
Directory (tenant) ID
</div>
}
placeholder={"5d60468a-65b7-45eb-a61a-53ecfbcd1ea3"}
value={tenantId}
onChange={(e) => setTenantId(e.target.value)}
/>
</div>
</div>
)}
{step == 5 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<FolderGit2 size={20} />
Groups to be synchronized
</p>
<div className={"mb-4 flex flex-col gap-1"}>
<div>
<HelpText className={"max-w-lg mt-2 text-sm"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Groups</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only groups that start with a
specific prefix, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
value={groupPrefixes}
onChange={setGroupPrefixes}
/>
</div>
</div>
)}
{step == 6 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<UserCircle size={18} />
Users to be synchronized
</p>
<div className={"mb-4 flex flex-col gap-1"}>
<div>
<HelpText className={"max-w-lg mt-2 text-sm"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Users</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only users that belong to a specific
group, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
addText={"Add user group filter"}
text={"User group starts with..."}
value={userGroupPrefixes}
onChange={setUserGroupPrefixes}
/>
</div>
</div>
)}
<ModalFooter className={"items-center gap-4"}>
{step > 0 && (
<Button
variant={"secondary"}
className={"w-full"}
onClick={() => setStep(step - 1)}
>
<IconArrowLeft size={16} />
Back
</Button>
)}
{step >= 0 && step < maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
disabled={isDisabled}
onClick={() => setStep(step + 1)}
>
{step == 0 ? "Get Started" : "Continue"}
<IconArrowRight size={16} />
</Button>
)}
{step == maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
disabled={isDisabled}
onClick={connect}
>
<Repeat size={16} />
Connect
</Button>
)}
</ModalFooter>
{step == 0 && (
<div
className={
"text-center z-0 mt-2.5 text-xs text-nb-gray-300 flex items-center justify-center gap-2 font-normal"
}
>
<Clock4 size={12} />
<div>
Estimated setup time:
<span className={"font-medium"}> 10-20 Minutes</span>
</div>
</div>
)}
</ModalContent>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -1,168 +0,0 @@
import Button from "@components/Button";
import FullTooltip from "@components/FullTooltip";
import { notify } from "@components/Notification";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import useFetchApi, { useApiCall } from "@utils/api";
import dayjs from "dayjs";
import { isEmpty } from "lodash";
import { RefreshCw, Settings } from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/google-workspace.png";
import {
AzureADIntegration,
GoogleWorkspaceIntegration,
IdentityProviderLog,
} from "@/interfaces/IdentityProvider";
import GoogleWorkspaceConfiguration from "@/modules/integrations/idp-sync/google-workspace/GoogleWorkspaceConfiguration";
import GoogleWorkspaceSetup from "@/modules/integrations/idp-sync/google-workspace/GoogleWorkspaceSetup";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
export const GoogleWorkspace = () => {
const { mutate } = useSWRConfig();
const [setupModal, setSetupModal] = useState(false);
const {
google: integration,
isAnyIntegrationEnabled,
isGoogleLoading,
} = useIntegrations();
const googleRequest = useApiCall<AzureADIntegration>(
"/integrations/google-idp",
);
const [enabled, setEnabled] = useState(
integration ? integration.enabled : false,
);
useEffect(() => {
setEnabled(integration?.enabled ?? false);
}, [integration]);
const toggleSwitch = async (state: boolean) => {
if (!integration) return setSetupModal(true);
notify({
title: "Google Workspace Integration",
description: `Google Workspace was successfully ${
state ? "enabled" : "disabled"
}`,
promise: googleRequest
.put(
{
enabled: state,
},
"/" + integration.id,
)
.then(() => {
mutate("/integrations/google-idp");
setEnabled(state);
}),
loadingMessage: "Updating integration...",
});
};
return isGoogleLoading ? (
<SkeletonIntegration loadingHeight={197} />
) : (
<>
<IntegrationCard
name="Google Workspace"
description="A flexible, innovative solution for people and organizations to achieve more."
url={{
title: "workspace.google.com",
href: "https://workspace.google.com/",
}}
image={integrationImage}
data={integration}
disabled={enabled ? false : isAnyIntegrationEnabled}
switchState={enabled}
onEnabledChange={toggleSwitch}
onSetup={() => setSetupModal(true)}
>
{integration && <ConfigurationButton config={integration} />}
</IntegrationCard>
<GoogleWorkspaceSetup
open={setupModal}
onOpenChange={setSetupModal}
onSuccess={() => setEnabled(true)}
/>
</>
);
};
type ConfigurationProps = {
config: GoogleWorkspaceIntegration;
};
const ConfigurationButton = ({ config }: ConfigurationProps) => {
const { data: logs } = useFetchApi<IdentityProviderLog[]>(
`/integrations/google-idp/${config.id}/logs`,
);
const { mutate } = useSWRConfig();
const syncRequest = useApiCall<{ response: boolean }>(
`/integrations/google-idp/${config.id}/sync`,
);
const [configModal, setConfigModal] = useState(false);
const forceSync = async () => {
notify({
title: "Google Workspace Integration",
description: `Google Workspace was successfully synced`,
loadingMessage: "Syncing integration...",
promise: syncRequest.post({}).then(() => {
mutate(`/integrations/google-idp/${config.id}/logs`);
}),
});
};
const lastSync = useMemo(() => {
if (isEmpty(logs)) return "Not synchronized";
return "Synced " + dayjs().to(logs?.[0]?.timestamp);
}, [logs]);
return (
<>
<div className={"flex gap-2"}>
<FullTooltip
content={
<div className={"text-xs"}>
Force synchronization of users and groups
</div>
}
disabled={!config.enabled}
className={"w-full"}
interactive={false}
>
<Button
variant={"secondary"}
size={"xs"}
className={"w-full items-center"}
onClick={forceSync}
disabled={!config.enabled}
>
<RefreshCw size={14} />
{lastSync}
</Button>
</FullTooltip>
<Button
variant={"secondary"}
size={"xs"}
className={"items-center"}
onClick={() => {
setConfigModal(true);
}}
>
<Settings size={14} />
</Button>
</div>
<GoogleWorkspaceConfiguration
open={configModal}
onOpenChange={setConfigModal}
/>
</>
);
};

View File

@@ -1,361 +0,0 @@
import Button from "@components/Button";
import HelpText from "@components/HelpText";
import { Input } from "@components/Input";
import { JSONFileUpload } from "@components/JSONFileUpload";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
} from "@components/modal/Modal";
import { notify } from "@components/Notification";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { useHasChanges } from "@hooks/useHasChanges";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import {
AlertOctagon,
Box,
Cog,
FolderGit2,
KeyRound,
RefreshCw,
UserCircle,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/google-workspace.png";
import { useDialog } from "@/contexts/DialogProvider";
import { GoogleWorkspaceIntegration } from "@/interfaces/IdentityProvider";
import { GroupPrefixInput } from "@/modules/integrations/idp-sync/GroupPrefixInput";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function GoogleWorkspaceConfiguration({
open,
onOpenChange,
onSuccess,
}: Props) {
const { google } = useIntegrations();
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
{google && (
<ConfigurationContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
config={google}
/>
)}
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
config: GoogleWorkspaceIntegration;
};
export function ConfigurationContent({ onSuccess, config }: ModalProps) {
const { mutate } = useSWRConfig();
const { confirm } = useDialog();
const [tab, setTab] = useState<string>("settings");
const googleRequest = useApiCall<GoogleWorkspaceIntegration>(
"/integrations/google-idp",
);
const accountKeyPlaceholder = "******************************";
const [serviceAccountKey, setServiceAccountKey] = useState(
accountKeyPlaceholder,
);
const [customerID, setCustomerID] = useState(config.customerId);
const [interval, setInterval] = useState(config.syncInterval.toString());
const [groupPrefixes, setGroupPrefixes] = useState<string[]>(
config.group_prefixes || [],
);
const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>(
config.user_group_prefixes || [],
);
const deleteIntegration = async () => {
const choice = await confirm({
title: `Delete integration?`,
description: "Are you sure you want to delete this integration?",
confirmText: "Delete",
cancelText: "Cancel",
type: "danger",
});
if (!choice) return;
notify({
title: "Google Workspace Integration",
description: `Google Workspace was successfully deleted`,
promise: googleRequest.del({}, `/${config.id}`).then(() => {
mutate("/integrations/google-idp");
onSuccess();
}),
loadingMessage: "Deleting integration...",
});
};
const updateIntegration = async () => {
notify({
title: "Google Workspace Integration",
description: `Google Workspace was successfully updated`,
promise: googleRequest
.put(
{
customerId: customerID,
service_account_key:
accountKeyPlaceholder == serviceAccountKey
? undefined
: serviceAccountKey,
sync_interval: interval ? parseInt(interval) : 300,
group_prefixes: groupPrefixes || [],
user_group_prefixes: userGroupPrefixes || [],
},
`/${config.id}`,
)
.then(() => {
mutate("/integrations/google-idp");
onSuccess();
}),
loadingMessage: "Updating integration...",
});
};
const { hasChanges } = useHasChanges([
customerID,
serviceAccountKey,
interval,
groupPrefixes,
userGroupPrefixes,
]);
return (
<ModalContent
maxWidthClass={cn("relative max-w-xl")}
showClose={true}
className={""}
autoFocus={false}
>
<GradientFadedBackground />
<IntegrationModalHeader
image={integrationImage}
title={"Google Workspace Configuration"}
description={"Sync your users and groups from Google Workspace."}
/>
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
className={"mt-6"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"settings"}>
<Cog
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Settings
</TabsTrigger>
<TabsTrigger value={"group-sync"}>
<FolderGit2
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Group Sync
</TabsTrigger>
<TabsTrigger value={"user-sync"}>
<UserCircle
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
User Sync
</TabsTrigger>
<TabsTrigger value={"danger"}>
<AlertOctagon
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Danger Zone
</TabsTrigger>
</TabsList>
<TabsContent value={"settings"} className={"px-8 text-sm"}>
<div className={"flex-col gap-3 flex"}>
<Input
type={"text"}
autoCorrect={"off"}
autoComplete={"off"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<Box size={16} />
Customer ID
</div>
}
placeholder={"62d3a656-c87d-4f30-a242-5b6347e29e9f"}
value={customerID}
onChange={(e) => setCustomerID(e.target.value)}
/>
<Input
autoCorrect={"off"}
type={"text"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<KeyRound size={16} />
Service Account Key
</div>
}
placeholder={"YdV7Q~JJ62Xl.LvYoBanxZR2sJA2va_3UbqvncY8"}
value={serviceAccountKey}
readOnly={true}
/>
<JSONFileUpload
value={serviceAccountKey}
onChange={(val) => setServiceAccountKey(btoa(val))}
/>
<div className={"flex justify-between mt-4"}>
<div>
<Label>Sync Interval</Label>
<HelpText className={"max-w-[300px]"}>
The interval in seconds when the synchronization should
happen.
</HelpText>
</div>
<Input
maxWidthClass={"max-w-[400px]"}
placeholder={"300"}
min={1}
max={99999}
value={interval}
type={"number"}
onChange={(e) => setInterval(e.target.value)}
customPrefix={
<RefreshCw size={16} className={"text-nb-gray-300"} />
}
customSuffix={"Seconds"}
/>
</div>
</div>
</TabsContent>
<TabsContent value={"group-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<FolderGit2 size={16} />
Synchronize Groups
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Groups</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only groups that start with a specific
prefix, you can add them below.
</HelpText>
</div>
<GroupPrefixInput value={groupPrefixes} onChange={setGroupPrefixes} />
</TabsContent>
<TabsContent value={"user-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<UserCircle size={16} />
Synchronize Users
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Users</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only users that belong to a specific
group, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
addText={"Add user group filter"}
text={"User group starts with..."}
value={userGroupPrefixes}
onChange={setUserGroupPrefixes}
/>
</TabsContent>
<TabsContent value={"danger"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<AlertOctagon size={16} />
Delete Integration
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
Deleting this integration will remove the ability to sync users
and groups from your IdP to NetBird. If you delete the integration
you will need to reconfigure it again to enable the
synchronization.
</HelpText>
</div>
<Button
variant={"danger"}
size={"xs"}
className={"mt-3"}
onClick={deleteIntegration}
>
Delete Integration
</Button>
</TabsContent>
</Tabs>
<div className={"h-6"}></div>
<ModalFooter className={"items-center gap-4"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
Cancel
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
disabled={!hasChanges}
onClick={updateIntegration}
>
Save
</Button>
</ModalFooter>
</ModalContent>
);
}

View File

@@ -1,632 +0,0 @@
import Button from "@components/Button";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { JSONFileUpload } from "@components/JSONFileUpload";
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
import { notify } from "@components/Notification";
import Steps from "@components/Steps";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { Lightbox } from "@components/ui/Lightbox";
import { Mark } from "@components/ui/Mark";
import { MinimalList } from "@components/ui/MinimalList";
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import { isEmpty } from "lodash";
import {
Box,
Clock4,
FolderCog2,
FolderGit2,
KeyRound,
Mail,
MailPlus,
PlusCircle,
Repeat,
Settings2,
Shield,
UserCircle,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/google-workspace.png";
import { GoogleWorkspaceIntegration } from "@/interfaces/IdentityProvider";
import googleAssignServiceAccount from "@/modules/integrations/idp-sync/google-workspace/images/google-assign-service-account.png";
import googleEditServiceAccount from "@/modules/integrations/idp-sync/google-workspace/images/google-edit-service-account.png";
import googlePrivilegesReview from "@/modules/integrations/idp-sync/google-workspace/images/google-privileges-review.png";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
import { GroupPrefixInput } from "../GroupPrefixInput";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function GoogleWorkspaceSetup({
open,
onOpenChange,
onSuccess,
}: Props) {
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
<SetupContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
/>
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
};
export function SetupContent({ onSuccess }: ModalProps) {
const { mutate } = useSWRConfig();
const googleRequest = useApiCall<GoogleWorkspaceIntegration>(
"/integrations/google-idp",
);
const [step, setStep] = useState(0);
const maxSteps = 9;
const [serviceAccountKey, setServiceAccountKey] = useState("");
const [customerID, setCustomerID] = useState("");
const [serviceAccountMail, setServiceAccountMail] = useState("");
const clientSecretEntered = !isEmpty(serviceAccountKey);
const customerIDEntered = !isEmpty(customerID);
const serviceAccountMailEntered = !isEmpty(serviceAccountMail);
const allEntered =
clientSecretEntered && customerIDEntered && serviceAccountMailEntered;
const isDisabled =
(step == 2 && !serviceAccountMailEntered) ||
(step == 3 && !clientSecretEntered) ||
(step == 7 && !customerIDEntered);
const [groupPrefixes, setGroupPrefixes] = useState<string[]>([]);
const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>([]);
const connect = async () => {
notify({
title: "Google Workspace Integration",
description: `Google Workspace was successfully connected to NetBird.`,
promise: googleRequest
.post({
service_account_key: btoa(serviceAccountKey), // Encode client secret to base64
customer_id: customerID,
group_prefixes: groupPrefixes || [],
user_group_prefixes: userGroupPrefixes || [],
})
.then(() => {
mutate("/integrations/google-idp");
onSuccess();
}),
loadingMessage: "Setting up integration...",
});
};
return (
<ModalContent
maxWidthClass={cn("relative", step == 0 ? "max-w-md" : "max-w-xl")}
showClose={true}
className={""}
onEscapeKeyDown={(e) => step > 0 && e.preventDefault()}
onInteractOutside={(e) => step > 0 && e.preventDefault()}
onPointerDownOutside={(e) => step > 0 && e.preventDefault()}
>
<GradientFadedBackground />
{step > 0 && (
<div className={"flex gap-2 w-full items-center justify-center mb-4"}>
{Array.from({ length: maxSteps }).map((_, index) => (
<div
key={index}
className={cn(
"w-8 h-1 rounded-full bg-nb-gray-800",
step >= index + 1 && "bg-netbird",
)}
/>
))}
</div>
)}
<IntegrationModalHeader
image={integrationImage}
title={"Connect NetBird with Google Workspace"}
description={
"Start syncing your users and groups from Google Workspace to NetBird. Follow the steps below to get started."
}
/>
{step == 0 && (
<div
className={
"px-8 py-3 flex z-0 flex-col gap-0 text-sm mb-3 text-center justify-center items-center"
}
>
<div
className={
"mt-6 text-base font-medium text-nb-gray-100 flex gap-2 items-center justify-center"
}
>
<Shield size={16} />
Required Permissions
</div>
<p className={"mt-2 !text-nb-gray-300 !leading-[1.5]"}>
Ensure that you have an an{" "}
<span className={"text-nb-gray-100 font-semibold"}>
Google Workspace user account
</span>{" "}
with the following{" "}
<span className={"text-nb-gray-100 font-semibold"}>
permissions
</span>
.{" "}
{
"If you don't have the required permissions, ask your workspace administrator to grant them to you."
}
</p>
<div
className={
"flex items-center flex-col gap-0 mt-2 w-full justify-center max-w-lg"
}
>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<PlusCircle size={14} className={"text-sky-500"} />
Create Google Workspace applications
</div>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<Settings2 size={14} className={"text-sky-500"} />
Manage Google Workspace applications
</div>
</div>
</div>
)}
{step == 1 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<UserCircle size={20} />
Create a service account
</p>
<Steps>
<Steps.Step step={1}>
<p>
Navigate to{" "}
<InlineLink
className={"inline"}
target={"_blank"}
href={"https://console.cloud.google.com/apis/credentials"}
>
API Credentials
</InlineLink>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>CREATE CREDENTIALS</Mark> at the top and select{" "}
<Mark>Service account</Mark>
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Fill in the form with the following values and click{" "}
<Mark>DONE</Mark>
</p>
</Steps.Step>
</Steps>
<MinimalList
data={[
{
label: "Service account name",
value: "NetBird",
},
{
label: "Service account ID",
value: "netbird",
},
]}
/>
</div>
)}
{step == 2 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Mail size={20} />
Get your service account email
</p>
<Steps>
<Steps.Step step={1}>
<p>
Navigate to{" "}
<InlineLink
className={"inline"}
target={"_blank"}
href={
"https://console.cloud.google.com/iam-admin/serviceaccounts"
}
>
Service Accounts
</InlineLink>
</p>
</Steps.Step>
<Steps.Step step={1}>
<p className={"font-normal"}>
Click <Mark>NetBird</Mark> to edit the service account. Copy the
service account email address.
</p>
<Lightbox image={googleEditServiceAccount} />
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Enter your service account email address
</p>
</Steps.Step>
</Steps>
<div className={"mb-4"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"flex items-center gap-2"}>
<Mail size={16} className={"text-nb-gray-300"} />
</div>
}
placeholder={"netbird@loadtests-347817.iam.gserviceaccount.com"}
value={serviceAccountMail}
onChange={(e) => setServiceAccountMail(e.target.value)}
/>
</div>
</div>
)}
{step == 3 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<KeyRound size={20} />
Create service account key
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
On the same page, now click the <Mark>Keys</Mark> tab, open the{" "}
<Mark>Add key</Mark> dropdown and select{" "}
<Mark>Create new key</Mark>
</p>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Select <Mark>JSON</Mark> as the key type and click{" "}
<Mark>Create</Mark>
</p>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p className={"font-normal"}>
Most browsers immediately download the new key and save it in a
download folder on your computer. Read how to manage and secure
your service keys{" "}
<InlineLink
href={
"https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys#temp-locations"
}
target={"_blank"}
>
here
</InlineLink>
.
</p>
</Steps.Step>
</Steps>
<div className={"mb-4 z-0 relative"}>
<JSONFileUpload
value={serviceAccountKey}
onChange={setServiceAccountKey}
/>
{serviceAccountKey && (
<div className={"mt-3"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"flex items-center gap-2"}>
<KeyRound size={16} className={"text-nb-gray-300"} />
</div>
}
placeholder={"YdV7Q~JJ62Xl.LvYoBanxZR2sJA2va_3UbqvncY8"}
value={btoa(serviceAccountKey)}
readOnly={true}
/>
</div>
)}
</div>
</div>
)}
{step == 4 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<FolderCog2 size={20} />
Create admin role
</p>
<Steps>
<Steps.Step step={1}>
<p>
Navigate to{" "}
<InlineLink
className={"inline"}
target={"_blank"}
href={"https://admin.google.com/ac/home"}
>
Admin Console
</InlineLink>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Select <Mark>Account</Mark> on the left menu and then click{" "}
<Mark>Admin Roles</Mark>
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Click <Mark>Create new role</Mark> and fill in the form with the
following values
</p>
</Steps.Step>
</Steps>
<MinimalList
data={[
{
label: "Name",
value: "User and Group Management ReadOnly",
},
{
label: "Description",
value: "User and Group Management ReadOnly",
},
]}
/>
</div>
)}
{step == 5 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Shield size={20} />
Add role privileges
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Scroll down to <Mark>Admin API privileges</Mark> and add the
following privileges to the role
</p>
<MinimalList
className={"mt-2 mb-0"}
data={[
{
label: "Users",
value: "Read",
},
{
label: "Groups",
value: "Read",
},
]}
/>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Verify preview of assigned Admin API privileges to ensure that
everything is properly configured, and then click{" "}
<Mark>CREATE ROLE</Mark>
</p>
<Lightbox image={googlePrivilegesReview} />
</Steps.Step>
</Steps>
</div>
)}
{step == 6 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<MailPlus size={20} />
Assign service account
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Click <Mark>Assign service accounts</Mark>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Enter your <Mark>E-Mail</Mark> and then click <Mark>ADD</Mark>
</p>
<MinimalList
className={"mt-2 mb-0"}
data={[
{
label: "E-Mail",
value: serviceAccountMail,
},
]}
/>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Click <Mark>ASSIGN ROLE</Mark>
</p>
<Lightbox image={googleAssignServiceAccount} />
</Steps.Step>
</Steps>
</div>
)}
{step == 7 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Enter Customer ID
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Navigate to{" "}
<InlineLink
target={"_blank"}
className={"inline"}
href={
"https://admin.google.com/ac/accountsettings/profile?hl=en_US"
}
>
Account Settings
</InlineLink>
</p>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Take note of the <Mark>Customer ID</Mark> and enter it below
</p>
</Steps.Step>
</Steps>
<div className={"mb-4 flex flex-col gap-3"}>
<Input
type={"text"}
className={"w-full"}
customPrefix={
<div className={"min-w-[165px] flex gap-2 items-center"}>
<Box size={16} />
Customer ID
</div>
}
placeholder={"C03f4c3po"}
value={customerID}
onChange={(e) => setCustomerID(e.target.value)}
/>
</div>
</div>
)}
{step == 8 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<FolderGit2 size={20} />
Groups to be synchronized
</p>
<div className={"mb-4 flex flex-col gap-1"}>
<div>
<HelpText className={"max-w-lg mt-2 text-sm"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Groups</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only groups that start with a
specific prefix, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
value={groupPrefixes}
onChange={setGroupPrefixes}
/>
</div>
</div>
)}
{step == 9 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<UserCircle size={18} />
Users to be synchronized
</p>
<div className={"mb-4 flex flex-col gap-1"}>
<div>
<HelpText className={"max-w-lg mt-2 text-sm"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Users</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only users that belong to a specific
group, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
addText={"Add user group filter"}
text={"User group starts with..."}
value={userGroupPrefixes}
onChange={setUserGroupPrefixes}
/>
</div>
</div>
)}
<ModalFooter className={"items-center gap-4"}>
{step > 0 && (
<Button
variant={"secondary"}
className={"w-full"}
onClick={() => setStep(step - 1)}
>
<IconArrowLeft size={16} />
Back
</Button>
)}
{step >= 0 && step < maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
disabled={isDisabled}
onClick={() => setStep(step + 1)}
>
{step == 0 ? "Get Started" : "Continue"}
<IconArrowRight size={16} />
</Button>
)}
{step == maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
disabled={!allEntered}
onClick={connect}
>
<Repeat size={16} />
Connect
</Button>
)}
</ModalFooter>
{step == 0 && (
<div
className={
"text-center z-0 mt-2.5 text-xs text-nb-gray-300 flex items-center justify-center gap-2 font-normal"
}
>
<Clock4 size={12} />
<div>
Estimated setup time:
<span className={"font-medium"}> 10-20 Minutes</span>
</div>
</div>
)}
</ModalContent>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -1,131 +0,0 @@
import Button from "@components/Button";
import { notify } from "@components/Notification";
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
import useFetchApi, { useApiCall } from "@utils/api";
import dayjs from "dayjs";
import { RefreshCw, Settings } from "lucide-react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/okta.png";
import {
IdentityProviderLog,
OktaIntegration,
} from "@/interfaces/IdentityProvider";
import OktaConfiguration from "@/modules/integrations/idp-sync/okta-scim/OktaConfiguration";
import OktaSetup from "@/modules/integrations/idp-sync/okta-scim/OktaSetup";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
export const Okta = () => {
const { mutate } = useSWRConfig();
const [setupModal, setSetupModal] = useState(false);
const {
okta: integration,
isAnyIntegrationEnabled,
isOktaLoading,
} = useIntegrations();
const oktaRequest = useApiCall<OktaIntegration>(
"/integrations/okta-scim-idp",
);
const [enabled, setEnabled] = useState(
integration ? integration.enabled : false,
);
useEffect(() => {
setEnabled(integration?.enabled ?? false);
}, [integration]);
const toggleSwitch = async (state: boolean) => {
if (!integration) return setSetupModal(true);
notify({
title: "Okta Integration",
description: `Okta was successfully ${state ? "enabled" : "disabled"}`,
promise: oktaRequest
.put(
{
enabled: state,
},
"/" + integration.id,
)
.then(() => {
mutate("/integrations/okta-scim-idp");
setEnabled(state);
}),
loadingMessage: "Updating integration...",
});
};
return isOktaLoading ? (
<SkeletonIntegration loadingHeight={197} />
) : (
<>
<IntegrationCard
name="Okta"
description="Okta is a platform to provision and manage user accounts in cloud-based applications."
url={{
title: "okta.com",
href: "https://www.okta.com/",
}}
image={integrationImage}
data={integration}
disabled={enabled ? false : isAnyIntegrationEnabled}
switchState={enabled}
onEnabledChange={toggleSwitch}
onSetup={() => setSetupModal(true)}
>
{integration && <ConfigurationButton config={integration} />}
</IntegrationCard>
<OktaSetup
open={setupModal}
onOpenChange={setSetupModal}
onSuccess={() => {
setEnabled(true);
mutate("/integrations/okta-scim-idp");
}}
/>
</>
);
};
type ConfigurationProps = {
config: OktaIntegration;
};
const ConfigurationButton = ({ config }: ConfigurationProps) => {
const { data: logs } = useFetchApi<IdentityProviderLog[]>(
`/integrations/okta-scim-idp/${config.id}/logs`,
);
const [configModal, setConfigModal] = useState(false);
return (
<>
<div className={"flex gap-2"}>
<Button
variant={"default-outline"}
size={"xs"}
className={"w-full items-center pointer-events-none"}
disabled={!config.enabled}
>
<RefreshCw size={14} />
Synced {dayjs().to(logs?.[0]?.timestamp)}
</Button>
<Button
variant={"secondary"}
size={"xs"}
className={"items-center"}
onClick={() => {
setConfigModal(true);
}}
>
<Settings size={14} />
</Button>
</div>
<OktaConfiguration open={configModal} onOpenChange={setConfigModal} />
</>
);
};

View File

@@ -1,333 +0,0 @@
import Button from "@components/Button";
import Card from "@components/Card";
import HelpText from "@components/HelpText";
import { Label } from "@components/Label";
import {
Modal,
ModalClose,
ModalContent,
ModalFooter,
} from "@components/modal/Modal";
import { notify } from "@components/Notification";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { useHasChanges } from "@hooks/useHasChanges";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import {
AlertOctagon,
Cog,
FolderGit2,
KeyRound,
RefreshCcw,
UserCircle,
} from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import integrationImage from "@/assets/integrations/entra-id.png";
import { useDialog } from "@/contexts/DialogProvider";
import { OktaIntegration } from "@/interfaces/IdentityProvider";
import { GroupPrefixInput } from "@/modules/integrations/idp-sync/GroupPrefixInput";
import { useIntegrations } from "@/modules/integrations/idp-sync/useIntegrations";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function OktaConfiguration({
open,
onOpenChange,
onSuccess,
}: Props) {
const { okta } = useIntegrations();
return (
<>
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
{okta && (
<ConfigurationContent
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
config={okta}
/>
)}
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
config: OktaIntegration;
};
export function ConfigurationContent({ onSuccess, config }: ModalProps) {
const { mutate } = useSWRConfig();
const { confirm } = useDialog();
const [tab, setTab] = useState<string>("settings");
const oktaRequest = useApiCall<OktaIntegration>(
"/integrations/okta-scim-idp",
);
const clientSecretPlaceholder = "******************************";
const [authToken, setAuthToken] = useState(
config.auth_token || clientSecretPlaceholder,
);
const [groupPrefixes, setGroupPrefixes] = useState<string[]>(
config.group_prefixes || [],
);
const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>(
config.user_group_prefixes || [],
);
const { hasChanges, updateRef } = useHasChanges([
authToken,
groupPrefixes,
userGroupPrefixes,
]);
const regenerateAuthToken = async () => {
const choice = await confirm({
title: `Regenerate Auth Token?`,
description:
"Are you sure you want to regenerate the auth token? You will need to update the token in your Okta configuration.",
confirmText: "Regenerate",
cancelText: "Cancel",
type: "default",
});
if (!choice) return;
notify({
title: "Okta Integration",
description: `Auth token for Okta was successfully regenerated`,
promise: oktaRequest.post({}, `/${config.id}/token`).then((r) => {
mutate("/integrations/okta-scim-idp");
setAuthToken(r.auth_token);
updateRef([r.auth_token, groupPrefixes, userGroupPrefixes]);
}),
loadingMessage: "Updating your auth token...",
});
};
const deleteIntegration = async () => {
const choice = await confirm({
title: `Delete integration?`,
description: "Are you sure you want to delete this integration?",
confirmText: "Delete",
cancelText: "Cancel",
type: "danger",
});
if (!choice) return;
notify({
title: "Okta Integration",
description: `Okta was successfully deleted`,
promise: oktaRequest.del({}, `/${config.id}`).then(() => {
mutate("/integrations/okta-scim-idp");
onSuccess();
}),
loadingMessage: "Deleting integration...",
});
};
const updateIntegration = async () => {
notify({
title: "Okta Integration",
description: `Okta was successfully updated`,
promise: oktaRequest
.put(
{
group_prefixes: groupPrefixes || [],
user_group_prefixes: userGroupPrefixes || [],
},
`/${config.id}`,
)
.then(() => {
mutate("/integrations/okta-scim-idp");
onSuccess();
}),
loadingMessage: "Updating integration...",
});
};
return (
<ModalContent
maxWidthClass={cn("relative max-w-xl")}
showClose={true}
className={""}
autoFocus={false}
>
<GradientFadedBackground />
<IntegrationModalHeader
image={integrationImage}
title={"Okta Configuration"}
description={"Sync your users and groups from Okta to NetBird."}
/>
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
className={"mt-6"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"settings"}>
<Cog
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Settings
</TabsTrigger>
<TabsTrigger value={"group-sync"}>
<FolderGit2
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Group Sync
</TabsTrigger>
<TabsTrigger value={"user-sync"}>
<UserCircle
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
User Sync
</TabsTrigger>
<TabsTrigger value={"danger"}>
<AlertOctagon
size={16}
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
/>
Danger Zone
</TabsTrigger>
</TabsList>
<TabsContent value={"settings"} className={"px-8 text-sm"}>
<div className={"flex-col gap-3 flex"}>
<Card className={"w-full"}>
<Card.List>
<Card.ListItem
copy={!authToken.includes("*")}
copyText={"Auth token"}
label={
<>
<KeyRound size={16} />
Auth Token
</>
}
value={authToken}
/>
</Card.List>
</Card>
<Button variant={"secondary"} onClick={regenerateAuthToken}>
<RefreshCcw size={16} />
Regenerate Auth Token
</Button>
</div>
</TabsContent>
<TabsContent value={"group-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<FolderGit2 size={16} />
Synchronize Groups
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Groups</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only groups that start with a specific
prefix, you can add them below.
</HelpText>
</div>
<GroupPrefixInput value={groupPrefixes} onChange={setGroupPrefixes} />
</TabsContent>
<TabsContent value={"user-sync"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<UserCircle size={16} />
Synchronize Users
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
By default,{" "}
<span className={"text-netbird font-semibold"}>All Users</span>{" "}
will be synchronized from your IdP to NetBird. <br />
If you want to synchronize only users that belong to a specific
group, you can add them below.
</HelpText>
</div>
<GroupPrefixInput
addText={"Add user group filter"}
text={"User group starts with..."}
value={userGroupPrefixes}
onChange={setUserGroupPrefixes}
/>
</TabsContent>
<TabsContent value={"danger"} className={"px-8"}>
<div>
<Label>
<div className={"flex gap-2 items-center"}>
<AlertOctagon size={16} />
Delete Integration
</div>
</Label>
<HelpText className={"max-w-lg mt-2"}>
Deleting this integration will remove the ability to sync users
and groups from your IdP to NetBird. If you delete the integration
you will need to reconfigure it again to enable the
synchronization.
</HelpText>
</div>
<Button
variant={"danger"}
size={"xs"}
className={"mt-3"}
onClick={deleteIntegration}
>
Delete Integration
</Button>
</TabsContent>
</Tabs>
<div className={"h-6"}></div>
<ModalFooter className={"items-center gap-4"}>
<ModalClose asChild={true}>
<Button variant={"secondary"} className={"w-full"}>
Cancel
</Button>
</ModalClose>
<Button
variant={"primary"}
className={"w-full"}
disabled={!hasChanges}
onClick={updateIntegration}
>
Save
</Button>
</ModalFooter>
</ModalContent>
);
}

View File

@@ -1,491 +0,0 @@
import Button from "@components/Button";
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
import Steps from "@components/Steps";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import { Lightbox } from "@components/ui/Lightbox";
import { Mark } from "@components/ui/Mark";
import { MinimalList } from "@components/ui/MinimalList";
import {
IconArrowLeft,
IconArrowRight,
IconCirclePlus,
} from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn } from "@utils/helpers";
import { isEmpty } from "lodash";
import {
Box,
Clock4,
FolderGit2,
PlusCircle,
Settings2,
Share2,
Shield,
UserCircle,
} from "lucide-react";
import React, { useEffect, useState } from "react";
import integrationImage from "@/assets/integrations/okta.png";
import { OktaIntegration } from "@/interfaces/IdentityProvider";
import oktaGroupsAssignments from "@/modules/integrations/idp-sync/okta-scim/images/okta-groups-assignments.png";
import oktaSAMLConfig from "@/modules/integrations/idp-sync/okta-scim/images/okta-saml-configuration.png";
import oktaSCIMProvisioning from "@/modules/integrations/idp-sync/okta-scim/images/okta-scim-provisioning-enabled.png";
import oktaSCIMToApp from "@/modules/integrations/idp-sync/okta-scim/images/okta-scim-to-app-sync-enabled.png";
import oktaSyncGroups from "@/modules/integrations/idp-sync/okta-scim/images/okta-sync-groups.png";
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
};
export default function OktaSetup({ open, onOpenChange, onSuccess }: Props) {
const [authToken, setAuthToken] = useState("");
return (
<>
<Modal open={open} onOpenChange={onOpenChange}>
{open && (
<SetupContent
authToken={authToken}
setAuthToken={setAuthToken}
onSuccess={() => {
onOpenChange(false);
onSuccess && onSuccess();
}}
/>
)}
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
authToken: string;
setAuthToken: (token: string) => void;
};
export function SetupContent({
onSuccess,
authToken,
setAuthToken,
}: ModalProps) {
const integrationsRequest = useApiCall<OktaIntegration[]>(
"/integrations/okta-scim-idp",
);
const authTokenRequest = useApiCall<OktaIntegration>(
"/integrations/okta-scim-idp",
).post;
const [step, setStep] = useState(0);
const maxSteps = 7;
// const [groupPrefixes, setGroupPrefixes] = useState<string[]>([]);
// const [userGroupPrefixes, setUserGroupPrefixes] = useState<string[]>([]);
useEffect(() => {
const getAuthToken = async () => {
const integration = await integrationsRequest.get();
if (!isEmpty(integration)) {
const integrationId = integration[0].id;
if (authToken != "") return authToken;
const okta = await authTokenRequest({}, `${integrationId}/token`);
if (!okta) return "";
return okta.auth_token;
} else {
const okta = await authTokenRequest({});
if (!okta) return "";
return okta.auth_token;
}
};
getAuthToken().then((t) => setAuthToken(t));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ModalContent
maxWidthClass={cn(
"relative",
step == 0 ? "max-w-md" : step == 4 ? "max-w-3xl" : "max-w-2xl",
)}
showClose={true}
className={""}
onEscapeKeyDown={(e) => step > 0 && e.preventDefault()}
onInteractOutside={(e) => step > 0 && e.preventDefault()}
onPointerDownOutside={(e) => step > 0 && e.preventDefault()}
>
<GradientFadedBackground />
{step > 0 && (
<div className={"flex gap-2 w-full items-center justify-center mb-4"}>
{Array.from({ length: maxSteps }).map((_, index) => (
<div
key={index}
className={cn(
"w-8 h-1 rounded-full bg-nb-gray-800",
step >= index + 1 && "bg-netbird",
)}
/>
))}
</div>
)}
<IntegrationModalHeader
image={integrationImage}
title={"Connect NetBird with Okta"}
description={
"Start syncing your users and groups from Okta to NetBird. Follow the steps below to get started."
}
/>
{step == 0 && (
<div
className={
"px-8 py-3 flex z-0 flex-col gap-0 text-sm mb-3 text-center justify-center items-center"
}
>
<div
className={
"mt-6 text-base font-medium text-nb-gray-100 flex gap-2 items-center justify-center"
}
>
<Shield size={16} />
Required Permissions
</div>
<p className={"mt-2 !text-nb-gray-300 !leading-[1.5]"}>
Ensure that you have an an{" "}
<span className={"text-nb-gray-100 font-semibold"}>
Okta user account
</span>{" "}
with the following{" "}
<span className={"text-nb-gray-100 font-semibold"}>
permissions
</span>
.{" "}
{
"If you don't have the required permissions, ask your Okta administrator to grant them to you."
}
</p>
<div
className={
"flex items-center flex-col gap-0 mt-2 w-full justify-center max-w-lg"
}
>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<PlusCircle size={14} className={"text-sky-500"} />
Add Okta applications
</div>
<div
className={
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
}
>
<Settings2 size={14} className={"text-sky-500"} />
Configure Okta applications
</div>
</div>
</div>
)}
{step == 1 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Create and configure Okta application
</p>
<Steps>
<Steps.Step step={1}>
<p>Navigate to your Okta Admin Dashboard</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>Applications</Mark> in the left menu then click on
<Mark>Applications</Mark>
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Click <Mark>Create App Integration</Mark>, select{" "}
<Mark>SAML 2.0</Mark> and click <Mark>Next</Mark>
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 2 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<IconCirclePlus size={20} />
Create SAML Integration
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Enter <Mark copy>NetBird SCIM</Mark> as the{" "}
<Mark>App name</Mark> and click <Mark>Next</Mark>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Enter <Mark copy>http://localhost</Mark> as the Single sign-on
URL and Audience URI (SP Entity ID) and click <Mark>Next</Mark>
</p>
<Lightbox image={oktaSAMLConfig} />
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Select App type as
<Mark>This is an internal app that we have created</Mark>
and click <Mark>Finish</Mark>
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 3 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Share2 size={20} />
Enable and configure SCIM provisioning
</p>
<Steps>
<Steps.Step step={1}>
<p>Navigate to your Okta Admin Dashboard</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Click <Mark>Applications</Mark> in the left menu then click on
<Mark>Applications</Mark>
</p>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Select the <Mark>NetBird SCIM</Mark> application we created
earlier
</p>
</Steps.Step>
<Steps.Step step={4}>
<p className={"font-normal"}>
Click <Mark>General</Mark> tab and in <Mark>App Settings</Mark>{" "}
click <Mark>Edit</Mark> to update the settings
</p>
</Steps.Step>
<Steps.Step step={5} line={false}>
<p className={"font-normal"}>
Tick <Mark>Enable SCIM provisioning</Mark> and click{" "}
<Mark>Save</Mark>
</p>
<Lightbox image={oktaSCIMProvisioning} />
</Steps.Step>
</Steps>
</div>
)}
{step == 4 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Share2 size={20} />
Enable and configure SCIM provisioning
</p>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
Click <Mark>Provisioning</Mark> tab and under{" "}
<Mark>SCIM connection</Mark> click
<Mark>Edit</Mark>
</p>
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Fill in the form with the following details
</p>
<MinimalList
data={[
{
label: "SCIM connector base URL",
value: "https://api.netbird.io/api/scim/v2",
},
{
label: "Unique identifier field for users",
value: "userName",
},
{
label: "Supported provisioning actions",
value: (
<div className={"text-right"}>
Push New Users <br />
Push Profile Updates <br />
Push Groups
</div>
),
noCopy: true,
},
{
label: "Authentication Mode",
value: "HTTP Header",
noCopy: true,
},
{
label: "Authorization (Bearer)",
value: authToken,
},
]}
/>
</Steps.Step>
<Steps.Step step={3}>
<p className={"font-normal"}>
Click on <Mark>Test Connector Configuration</Mark> to verify if
the SCIM configuration is working. After the test is completed,
make sure <Mark>Create Users</Mark>,{" "}
<Mark>Update User Attributes</Mark>, and{" "}
<Mark>Push Groups</Mark> were successful.
</p>
</Steps.Step>
<Steps.Step step={4} line={false}>
<p className={"font-normal"}>
Click <Mark>Save</Mark>
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 5 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<Box size={20} />
Configure SCIM provisioning to NetBird
</p>
<Steps>
<Steps.Step step={1}>
<p>
Go to the <Mark>Provisioning</Mark> tab, and select the{" "}
<Mark>To App</Mark> settings and click <Mark>Edit</Mark>
</p>
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Enable <Mark>Create Users</Mark>,{" "}
<Mark>Update User Attributes</Mark>, and{" "}
<Mark>Deactivate Users</Mark> and click <Mark>Save</Mark>
</p>
<Lightbox image={oktaSCIMToApp} />
</Steps.Step>
</Steps>
</div>
)}
{step == 6 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<UserCircle size={20} />
Assign members to NetBird
</p>
<Steps>
<Steps.Step step={1}>
<p>
Go to the <Mark>Assignments</Mark> tab, select the{" "}
<Mark>Assign</Mark> and click <Mark>Assign to Groups</Mark>
</p>
<Lightbox image={oktaGroupsAssignments} />
</Steps.Step>
<Steps.Step step={2}>
<p className={"font-normal"}>
Select the groups you want to provision, and then select{" "}
<Mark>Assign</Mark> and click <Mark>Save and Go Back</Mark>
</p>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Select <Mark>Done</Mark> after you have finished assigning
groups. At this point, all members of the groups assigned to the
application will be synced to NetBird.
</p>
</Steps.Step>
</Steps>
</div>
)}
{step == 7 && (
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
<p className={"font-medium flex gap-3 items-center text-base"}>
<FolderGit2 size={20} />
Assign groups to NetBird
</p>
<Steps>
<Steps.Step step={1}>
<p>
Go to the <Mark>Push Groups</Mark> tab, select{" "}
<Mark>Push Groups</Mark> and click{" "}
<Mark>Find groups by name</Mark>
</p>
<Lightbox image={oktaSyncGroups} />
</Steps.Step>
<Steps.Step step={2} line={false}>
<p className={"font-normal"}>
Search groups to push and then click <Mark>Save</Mark>. The
selected groups will then be synced to NetBird.
</p>
</Steps.Step>
</Steps>
</div>
)}
<ModalFooter className={"items-center gap-4"}>
{step > 0 && (
<Button
variant={"secondary"}
className={"w-full"}
onClick={() => setStep(step - 1)}
>
<IconArrowLeft size={16} />
Back
</Button>
)}
{step >= 0 && step < maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
onClick={() => setStep(step + 1)}
disabled={authToken == ""}
>
{step == 0 ? "Get Started" : "Continue"}
<IconArrowRight size={16} />
</Button>
)}
{step == maxSteps && (
<Button
variant={"primary"}
className={"w-full"}
onClick={() => {
onSuccess();
}}
>
Finish Setup
</Button>
)}
</ModalFooter>
{step == 0 && (
<div
className={
"text-center z-0 mt-2.5 text-xs text-nb-gray-300 flex items-center justify-center gap-2 font-normal"
}
>
<Clock4 size={12} />
<div>
Estimated setup time:
<span className={"font-medium"}> 10-20 Minutes</span>
</div>
</div>
)}
</ModalContent>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,35 +0,0 @@
import useFetchApi from "@utils/api";
import {
AzureADIntegration,
GoogleWorkspaceIntegration,
OktaIntegration,
} from "@/interfaces/IdentityProvider";
export const useIntegrations = () => {
const { data: azureIntegrations, isLoading: isAzureLoading } = useFetchApi<
AzureADIntegration[]
>("/integrations/azure-idp");
const { data: googleIntegrations, isLoading: isGoogleLoading } = useFetchApi<
GoogleWorkspaceIntegration[]
>("/integrations/google-idp");
const { data: oktaIntegration, isLoading: isOktaLoading } = useFetchApi<
OktaIntegration[]
>("/integrations/okta-scim-idp");
const azure = azureIntegrations?.[0];
const google = googleIntegrations?.[0];
const okta = oktaIntegration?.[0];
const isAnyIntegrationEnabled =
azure?.enabled || google?.enabled || okta?.enabled;
return {
azure,
google,
okta,
isAnyIntegrationEnabled,
isAzureLoading,
isGoogleLoading,
isOktaLoading,
};
};

View File

@@ -16,8 +16,7 @@ import {
import * as Tabs from "@radix-ui/react-tabs";
import { useApiCall } from "@utils/api";
import { cn, isInt } from "@utils/helpers";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import { CalendarClock, ShieldIcon, TimerReset, VoteIcon } from "lucide-react";
import { CalendarClock, ShieldIcon, TimerReset } from "lucide-react";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import SettingsIcon from "@/assets/icons/SettingsIcon";
@@ -143,22 +142,6 @@ export default function AuthenticationTab({ account }: Props) {
</div>
<div className={"flex flex-col gap-6 w-full mt-8"}>
{(isLocalDev() || isNetBirdHosted()) && (
<div>
<FancyToggleSwitch
value={peerApproval}
onChange={setPeerApproval}
label={
<>
<VoteIcon size={15} />
Peer approval
</>
}
helpText={"Require peers to be approved by an administrator."}
/>
</div>
)}
<div>
<FancyToggleSwitch
value={loginExpiration}