Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4716cdad | ||
|
|
859916b1df | ||
|
|
80ce7d21b0 | ||
|
|
06fdbd8ec4 | ||
|
|
973cceff79 | ||
|
|
f4a2d6fae8 | ||
|
|
cb922b46b7 | ||
|
|
4c56ae704c |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
FlagIcon,
|
||||
Globe,
|
||||
History,
|
||||
LockIcon,
|
||||
MapPin,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
@@ -50,6 +51,7 @@ import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
@@ -124,6 +126,8 @@ function PeerOverview() {
|
||||
});
|
||||
};
|
||||
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<RoutesProvider>
|
||||
@@ -148,29 +152,31 @@ function PeerOverview() {
|
||||
/>
|
||||
<TextWithTooltip text={name} maxChars={30} />
|
||||
|
||||
<Modal
|
||||
open={showEditNameModal}
|
||||
onOpenChange={setShowEditNameModal}
|
||||
>
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<EditNameModal
|
||||
onSuccess={(newName) => {
|
||||
setName(newName);
|
||||
setShowEditNameModal(false);
|
||||
}}
|
||||
peer={peer}
|
||||
initialName={name}
|
||||
key={showEditNameModal ? 1 : 0}
|
||||
/>
|
||||
</Modal>
|
||||
{!isUser && (
|
||||
<Modal
|
||||
open={showEditNameModal}
|
||||
onOpenChange={setShowEditNameModal}
|
||||
>
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</div>
|
||||
</ModalTrigger>
|
||||
<EditNameModal
|
||||
onSuccess={(newName) => {
|
||||
setName(newName);
|
||||
setShowEditNameModal(false);
|
||||
}}
|
||||
peer={peer}
|
||||
initialName={name}
|
||||
key={showEditNameModal ? 1 : 0}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</h1>
|
||||
<LoginExpiredBadge loginExpired={peer.login_expired} />
|
||||
</div>
|
||||
@@ -192,7 +198,7 @@ function PeerOverview() {
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={() => updatePeer()}
|
||||
disabled={!hasChanges}
|
||||
disabled={!hasChanges || isUser}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
@@ -210,18 +216,32 @@ function PeerOverview() {
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<IconInfoCircle size={14} />
|
||||
<span>
|
||||
Login expiration is disabled for all peers added with an
|
||||
setup-key.
|
||||
</span>
|
||||
{!peer.user_id ? (
|
||||
<>
|
||||
<>
|
||||
<IconInfoCircle size={14} />
|
||||
<span>
|
||||
Login expiration is disabled for all peers added
|
||||
with an setup-key.
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
className={"w-full block"}
|
||||
disabled={!!peer.user_id}
|
||||
disabled={!!peer.user_id && !isUser}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
disabled={!peer.user_id}
|
||||
disabled={!peer.user_id || isUser}
|
||||
value={loginExpiration}
|
||||
onChange={setLoginExpiration}
|
||||
label={
|
||||
@@ -235,33 +255,74 @@ function PeerOverview() {
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
onChange={(set) =>
|
||||
!set
|
||||
? setSsh(false)
|
||||
: openSSHDialog().then((confirm) => setSsh(confirm))
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<TerminalSquare size={16} />
|
||||
SSH Access
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
/>
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={!isUser}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
disabled={isUser}
|
||||
onChange={(set) =>
|
||||
!set
|
||||
? setSsh(false)
|
||||
: openSSHDialog().then((confirm) => setSsh(confirm))
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<TerminalSquare size={16} />
|
||||
SSH Access
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable the SSH server on this peer to access the machine via an secure shell."
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
|
||||
<div>
|
||||
<Label>Assigned Groups</Label>
|
||||
<HelpText>
|
||||
Use groups to control what this peer can access.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
peer={peer}
|
||||
/>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={!isUser}
|
||||
>
|
||||
<PeerGroupSelector
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
peer={peer}
|
||||
/>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +330,7 @@ function PeerOverview() {
|
||||
|
||||
<Separator />
|
||||
|
||||
{isLinux ? (
|
||||
{isLinux && !isUser ? (
|
||||
<div className={"px-8 py-6"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
|
||||
@@ -17,11 +17,15 @@ import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function Peers() {
|
||||
const { isUser } = useLoggedInUser();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isUser ? <PeersDefaultView /> : <PeersView />}
|
||||
{permission?.dashboard_view === "blocked" ? (
|
||||
<PeersBlockedView />
|
||||
) : (
|
||||
<PeersView />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -78,11 +82,11 @@ function PeersView() {
|
||||
);
|
||||
}
|
||||
|
||||
function PeersDefaultView() {
|
||||
function PeersBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new peer to your network</h1>
|
||||
<h1>Add new device to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
After that you should be connected. If you have further questions
|
||||
|
||||
@@ -2,20 +2,35 @@
|
||||
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import { AlertOctagonIcon, FolderGit2Icon, ShieldIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
AlertOctagonIcon,
|
||||
FolderGit2Icon,
|
||||
LockIcon,
|
||||
ShieldIcon,
|
||||
} from "lucide-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";
|
||||
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
||||
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
||||
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}>
|
||||
@@ -28,6 +43,10 @@ export default function NetBirdSettings() {
|
||||
<FolderGit2Icon size={14} />
|
||||
Groups
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="permissions">
|
||||
<LockIcon size={14} />
|
||||
Permissions
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
|
||||
<AlertOctagonIcon size={14} />
|
||||
Danger zone
|
||||
@@ -36,6 +55,7 @@ export default function NetBirdSettings() {
|
||||
<RestrictedAccess page={"Settings"}>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
{account && <AuthenticationTab account={account} />}
|
||||
{account && <PermissionsTab account={account} />}
|
||||
{account && <GroupsTab account={account} />}
|
||||
{account && <DangerZoneTab account={account} />}
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
src/assets/integrations/okta.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
@@ -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",
|
||||
|
||||
@@ -5,7 +5,9 @@ import * as React from "react";
|
||||
type Props = {
|
||||
data: {
|
||||
label: string;
|
||||
value: string;
|
||||
value: string | React.ReactNode;
|
||||
noCopy?: boolean;
|
||||
tooltip?: boolean;
|
||||
}[];
|
||||
className?: string;
|
||||
};
|
||||
@@ -16,10 +18,11 @@ export const MinimalList = ({ data, className }: Props) => {
|
||||
{data.map((item, index) => {
|
||||
return (
|
||||
<Card.ListItem
|
||||
copy
|
||||
copy={!item.noCopy}
|
||||
label={item.label}
|
||||
value={item.value}
|
||||
key={index}
|
||||
tooltip={item.tooltip !== false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function UserDropdown() {
|
||||
useHotkeys("shift+mod+l", () => logout(), []);
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
@@ -67,19 +68,23 @@ export default function UserDropdown() {
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
if (loggedInUser) {
|
||||
router.push(`/team/user?id=${loggedInUser.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<User2 size={14} />
|
||||
Profile Settings
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{permission.dashboard_view !== "blocked" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
if (loggedInUser) {
|
||||
router.push(`/team/user?id=${loggedInUser.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<User2 size={14} />
|
||||
Profile Settings
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem onClick={logoutSession}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<LogOutIcon size={14} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AnnouncementVariant } from "@components/ui/AnnouncementBanner";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import md5 from "crypto-js/md5";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
|
||||
const initialAnnouncements: Announcement[] = [];
|
||||
|
||||
@@ -12,6 +13,7 @@ export interface Announcement extends AnnouncementVariant {
|
||||
linkText?: string;
|
||||
isExternal?: boolean;
|
||||
closeable: boolean;
|
||||
isCloudOnly: boolean;
|
||||
}
|
||||
|
||||
interface AnnouncementInfo extends Announcement {
|
||||
@@ -28,6 +30,9 @@ const AnnouncementContext = React.createContext(
|
||||
bannerHeight: number;
|
||||
announcements?: AnnouncementInfo[];
|
||||
closeAnnouncement: (hash: string) => void;
|
||||
setAnnouncements: React.Dispatch<
|
||||
React.SetStateAction<AnnouncementInfo[] | undefined>
|
||||
>;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -39,8 +44,11 @@ export default function AnnouncementProvider({ children }: Props) {
|
||||
string[]
|
||||
>("netbird-closed-announcements", []);
|
||||
const [announcements, setAnnouncements] = useState<AnnouncementInfo[]>();
|
||||
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();
|
||||
const isOpen = !closedAnnouncements.some((h) => h === hash);
|
||||
@@ -48,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]);
|
||||
@@ -78,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>
|
||||
|
||||
@@ -3,7 +3,14 @@ import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { useIsMd } from "@utils/responsive";
|
||||
import { getLatestNetbirdRelease } from "@utils/version";
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { User } from "@/interfaces/User";
|
||||
import type { NetbirdRelease } from "@/interfaces/Version";
|
||||
@@ -32,13 +39,27 @@ export default function ApplicationProvider({ children }: Props) {
|
||||
const userRequest = useApiCall<User[]>("/users", true);
|
||||
const [show, setShow] = useState(false);
|
||||
const requestCalled = useRef(false);
|
||||
const maxTries = 3;
|
||||
|
||||
const populateCache = useCallback(
|
||||
async (tries = 0) => {
|
||||
if (tries >= maxTries) {
|
||||
setShow(true);
|
||||
return Promise.reject();
|
||||
}
|
||||
try {
|
||||
await userRequest.get().then(() => setShow(true));
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
setTimeout(() => populateCache(tries + 1), 500);
|
||||
}
|
||||
},
|
||||
[userRequest, setShow],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestCalled.current) {
|
||||
userRequest
|
||||
.get()
|
||||
.then(() => setShow(true))
|
||||
.catch(() => setShow(true));
|
||||
populateCache().then();
|
||||
requestCalled.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -17,10 +17,16 @@ const CountryContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function CountryProvider({ children }: Props) {
|
||||
const { isUser } = useLoggedInUser();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return isUser ? (
|
||||
children
|
||||
const getRegionByPeer = (peer: Peer) => "Unknown";
|
||||
|
||||
return permission?.dashboard_view != "full" ? (
|
||||
<CountryContext.Provider
|
||||
value={{ countries: [], isLoading: false, getRegionByPeer }}
|
||||
>
|
||||
{children}
|
||||
</CountryContext.Provider>
|
||||
) : (
|
||||
<CountryProviderContent>{children}</CountryProviderContent>
|
||||
);
|
||||
@@ -29,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"}
|
||||
|
||||
@@ -20,10 +20,10 @@ const GroupContext = React.createContext(
|
||||
|
||||
export default function GroupsProvider({ children }: Props) {
|
||||
const path = usePathname();
|
||||
const { isUser } = useLoggedInUser();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return isUser && path == "/peers" ? (
|
||||
children
|
||||
return path === "/peers" && permission.dashboard_view == "blocked" ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<GroupsProviderContent>{children}</GroupsProviderContent>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import useFetchApi from "@utils/api";
|
||||
import React, { useMemo } from "react";
|
||||
import { Permission } from "@/interfaces/Permission";
|
||||
import { User } from "@/interfaces/User";
|
||||
|
||||
type Props = {
|
||||
@@ -26,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>
|
||||
@@ -43,5 +44,19 @@ export const useLoggedInUser = () => {
|
||||
const isAdmin = loggedInUser ? loggedInUser?.role === "admin" : false;
|
||||
const isUser = !isOwner && !isAdmin;
|
||||
const isOwnerOrAdmin = isOwner || isAdmin;
|
||||
return { loggedInUser, isOwner, isAdmin, isUser, isOwnerOrAdmin } as const;
|
||||
|
||||
const permission = useMemo(() => {
|
||||
return {
|
||||
dashboard_view: loggedInUser?.permissions.dashboard_view || "blocked",
|
||||
} as Permission;
|
||||
}, [loggedInUser]);
|
||||
|
||||
return {
|
||||
loggedInUser,
|
||||
isOwner,
|
||||
isAdmin,
|
||||
isUser,
|
||||
isOwnerOrAdmin,
|
||||
permission,
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -10,5 +10,6 @@ export interface Account {
|
||||
jwt_groups_enabled: boolean;
|
||||
jwt_groups_claim_name: string;
|
||||
jwt_allow_groups: string[];
|
||||
regular_users_view_blocked: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ export interface AzureADIntegration {
|
||||
user_group_prefixes: string[];
|
||||
}
|
||||
|
||||
export interface OktaIntegration {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
group_prefixes: string[];
|
||||
user_group_prefixes: string[];
|
||||
auth_token: string;
|
||||
}
|
||||
|
||||
export interface IdentityProviderLog {
|
||||
id: number;
|
||||
level: string;
|
||||
|
||||
3
src/interfaces/Permission.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Permission {
|
||||
dashboard_view: "limited" | "full" | "blocked";
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Permission } from "@/interfaces/Permission";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email?: string;
|
||||
@@ -9,6 +11,7 @@ export interface User {
|
||||
is_service_user?: boolean;
|
||||
is_blocked?: boolean;
|
||||
last_login?: Date;
|
||||
permissions: Permission;
|
||||
}
|
||||
|
||||
export enum Role {
|
||||
|
||||
@@ -11,7 +11,6 @@ import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import OIDCProvider from "@/auth/OIDCProvider";
|
||||
import AnalyticsProvider from "@/contexts/AnalyticsProvider";
|
||||
import AnnouncementProvider from "@/contexts/AnnouncementProvider";
|
||||
import DialogProvider from "@/contexts/DialogProvider";
|
||||
import ErrorBoundaryProvider from "@/contexts/ErrorBoundary";
|
||||
import { GlobalThemeProvider } from "@/contexts/GlobalThemeProvider";
|
||||
@@ -36,11 +35,9 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
<GlobalThemeProvider>
|
||||
<ErrorBoundaryProvider>
|
||||
<OIDCProvider>
|
||||
<AnnouncementProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</AnnouncementProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</OIDCProvider>
|
||||
</ErrorBoundaryProvider>
|
||||
</GlobalThemeProvider>
|
||||
|
||||
@@ -9,7 +9,9 @@ import { useIsSm, useIsXs } from "@utils/responsive";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { XIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
|
||||
import AnnouncementProvider, {
|
||||
useAnnouncement,
|
||||
} from "@/contexts/AnnouncementProvider";
|
||||
import ApplicationProvider, {
|
||||
useApplicationContext,
|
||||
} from "@/contexts/ApplicationProvider";
|
||||
@@ -27,11 +29,13 @@ export default function DashboardLayout({
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<UsersProvider>
|
||||
<GroupsProvider>
|
||||
<CountryProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
</CountryProvider>
|
||||
</GroupsProvider>
|
||||
<AnnouncementProvider>
|
||||
<GroupsProvider>
|
||||
<CountryProvider>
|
||||
<DashboardPageContent>{children}</DashboardPageContent>
|
||||
</CountryProvider>
|
||||
</GroupsProvider>
|
||||
</AnnouncementProvider>
|
||||
</UsersProvider>
|
||||
</ApplicationProvider>
|
||||
);
|
||||
@@ -42,7 +46,7 @@ function DashboardPageContent({ children }: { children: React.ReactNode }) {
|
||||
const { mobileNavOpen, toggleMobileNav } = useApplicationContext();
|
||||
const isSm = useIsSm();
|
||||
const isXs = useIsXs();
|
||||
const { isUser } = useLoggedInUser();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
const navOpenPageWidth = isSm ? "50%" : isXs ? "65%" : "80%";
|
||||
const { bannerHeight } = useAnnouncement();
|
||||
@@ -154,7 +158,9 @@ function DashboardPageContent({ children }: { children: React.ReactNode }) {
|
||||
height: `calc(100vh - ${headerHeight + bannerHeight}px)`,
|
||||
}}
|
||||
>
|
||||
{!isUser && <Navigation hideOnMobile />}
|
||||
{permission.dashboard_view !== "blocked" && (
|
||||
<Navigation hideOnMobile />
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function NavbarWithDropdown() {
|
||||
|
||||
const { toggleMobileNav } = useApplicationContext();
|
||||
const { bannerHeight } = useAnnouncement();
|
||||
const { isUser } = useLoggedInUser();
|
||||
const { permission } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -62,7 +62,8 @@ export default function NavbarWithDropdown() {
|
||||
<Button
|
||||
className={cn(
|
||||
"!px-3 md:hidden",
|
||||
isUser && "opacity-0 pointer-events-none",
|
||||
permission.dashboard_view == "blocked" &&
|
||||
"opacity-0 pointer-events-none",
|
||||
)}
|
||||
variant={"default-outline"}
|
||||
onClick={toggleMobileNav}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FolderGit2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
@@ -38,6 +39,7 @@ export default function GroupsRow({
|
||||
peer,
|
||||
}: Props) {
|
||||
const { groups: allGroups } = useGroups();
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
// Get the group by the id
|
||||
const foundGroups = useMemo(() => {
|
||||
@@ -54,7 +56,7 @@ export default function GroupsRow({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setModal && setModal(true);
|
||||
setModal && !isUser && setModal(true);
|
||||
}}
|
||||
>
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
|
||||
@@ -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,87 +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 { 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={"#"} 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} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GoogleWorkspace />
|
||||
<AzureAD />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex flex-col gap-6 max-w-md 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 Identity Provider like Okta or
|
||||
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,158 +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 { RefreshCw, Settings } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, 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`);
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
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} />
|
||||
Synced {dayjs().to(logs?.[0]?.timestamp)}
|
||||
</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,27 +0,0 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import {
|
||||
AzureADIntegration,
|
||||
GoogleWorkspaceIntegration,
|
||||
} 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 azure = azureIntegrations?.[0];
|
||||
const google = googleIntegrations?.[0];
|
||||
|
||||
const isAnyIntegrationEnabled = azure?.enabled || google?.enabled;
|
||||
|
||||
return {
|
||||
azure,
|
||||
google,
|
||||
isAnyIntegrationEnabled,
|
||||
isAzureLoading,
|
||||
isGoogleLoading,
|
||||
};
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeerProvider from "@/contexts/PeerProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
@@ -133,6 +134,7 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
@@ -176,6 +178,8 @@ export default function PeersTable({ peers, isLoading }: Props) {
|
||||
"name",
|
||||
) as Group[]) || ([] as Group[]);
|
||||
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
onRowClick={(row) => router.push("/peer?id=" + row.original.id)}
|
||||
@@ -193,6 +197,7 @@ export default function PeersTable({ peers, isLoading }: Props) {
|
||||
ip: false,
|
||||
user_name: false,
|
||||
user_email: false,
|
||||
actions: !isUser,
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={
|
||||
@@ -229,6 +234,29 @@ export default function PeersTable({ peers, isLoading }: Props) {
|
||||
{(table) => (
|
||||
<>
|
||||
<ButtonGroup disabled={peers?.length == 0}>
|
||||
<ButtonGroup.Button
|
||||
disabled={peers?.length == 0}
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.setColumnFilters([
|
||||
{
|
||||
id: "connected",
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
id: "approval_required",
|
||||
value: undefined,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
variant={
|
||||
table.getColumn("connected")?.getFilterValue() == undefined
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
@@ -253,13 +281,12 @@ export default function PeersTable({ peers, isLoading }: Props) {
|
||||
Online
|
||||
</ButtonGroup.Button>
|
||||
<ButtonGroup.Button
|
||||
disabled={peers?.length == 0}
|
||||
onClick={() => {
|
||||
table.setPageIndex(0);
|
||||
table.setColumnFilters([
|
||||
{
|
||||
id: "connected",
|
||||
value: undefined,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
id: "approval_required",
|
||||
@@ -267,13 +294,14 @@ export default function PeersTable({ peers, isLoading }: Props) {
|
||||
},
|
||||
]);
|
||||
}}
|
||||
disabled={peers?.length == 0}
|
||||
variant={
|
||||
table.getColumn("connected")?.getFilterValue() == undefined
|
||||
table.getColumn("connected")?.getFilterValue() == false
|
||||
? "tertiary"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
All
|
||||
Offline
|
||||
</ButtonGroup.Button>
|
||||
</ButtonGroup>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
100
src/modules/settings/PermissionsTab.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import { notify } from "@components/Notification";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { GaugeIcon, LockIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import SettingsIcon from "@/assets/icons/SettingsIcon";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { Account } from "@/interfaces/Account";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export default function PermissionsTab({ account }: Props) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
|
||||
const [userViewBlocked, setUserViewBlocked] = useState<boolean>(
|
||||
account?.settings.regular_users_view_blocked ?? false,
|
||||
);
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([userViewBlocked]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
notify({
|
||||
title: "Permission Settings",
|
||||
description: "Permissions were updated successfully.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
regular_users_view_blocked: userViewBlocked,
|
||||
groups_propagation_enabled:
|
||||
account.settings?.groups_propagation_enabled,
|
||||
peer_login_expiration_enabled:
|
||||
account.settings?.peer_login_expiration_enabled,
|
||||
peer_login_expiration: account.settings?.peer_login_expiration,
|
||||
jwt_groups_enabled: account.settings?.jwt_groups_enabled,
|
||||
jwt_groups_claim_name: account.settings?.jwt_groups_claim_name,
|
||||
jwt_allow_groups: account.settings?.jwt_allow_groups,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([userViewBlocked]);
|
||||
}),
|
||||
loadingMessage: "Updating permissions...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"permissions"} className={"w-full"}>
|
||||
<div className={"p-default py-6 max-w-xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={"Settings"}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=permissions"}
|
||||
label={"Permissions"}
|
||||
icon={<LockIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>Permissions</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 mt-8 mb-3"}>
|
||||
<FancyToggleSwitch
|
||||
value={userViewBlocked}
|
||||
onChange={setUserViewBlocked}
|
||||
label={
|
||||
<>
|
||||
<GaugeIcon size={15} />
|
||||
Restrict dashboard for regular users
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Access to the dashboard will be limited and regular users will not be able to view any peers."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +100,11 @@ export function ServiceUserModalContent({ onSuccess }: ModalProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UserRoleSelector value={role as Role} onChange={setRole} />
|
||||
<UserRoleSelector
|
||||
value={role as Role}
|
||||
onChange={setRole}
|
||||
hideOwner={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function UserActionCell({ user, serviceUser = false }: Props) {
|
||||
const deleteRule = async () => {
|
||||
const name = user.name || "User";
|
||||
notify({
|
||||
title: name + "deleted",
|
||||
title: `'${name}' deleted`,
|
||||
description: "User was successfully deleted.",
|
||||
promise: userRequest.del("", `/${user.id}`).then(() => {
|
||||
mutate(`/users?service_user=${serviceUser}`);
|
||||
|
||||
@@ -26,16 +26,28 @@ async function apiRequest<T>(
|
||||
data?: any,
|
||||
) {
|
||||
const origin = config.apiOrigin;
|
||||
|
||||
const res = await oidcFetch(`${origin}/api${url}`, {
|
||||
method,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = (await res.json()) as ErrorResponse;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return (await res.json()) as T;
|
||||
try {
|
||||
if (!res.ok) {
|
||||
const error = (await res.json()) as ErrorResponse;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
} catch (e) {
|
||||
if (!res.ok) {
|
||||
const error = {
|
||||
code: res.status,
|
||||
message: res.statusText,
|
||||
} as ErrorResponse;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export function useNetBirdFetch(ignoreError: boolean = false) {
|
||||
@@ -160,6 +172,9 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
if (err.code == 500 && err.message == "internal server error") {
|
||||
return setError(err);
|
||||
}
|
||||
if (err.code > 400 && err.code <= 500) {
|
||||
return setError(err);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
};
|
||||
|
||||