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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 182 KiB |
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 117 KiB |
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 65 KiB |
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
|
||||