Compare commits

..
10 Commits
Author SHA1 Message Date
Eduard Gert 5e8048dd20 Push to docker 2024-04-03 13:35:39 +02:00
Eduard Gert 97255dd882 Add logging 2024-04-03 13:33:58 +02:00
Eduard Gert 6d4716cdad Remove integrations from public repo and sync changes (#369)
* Change icon size

* Remove integrations

* Add no cache header

* Add analytics event tracking

* Add small announcement improvements

* Remove peer approval setting

* Do not load countries when user has no permission

* Add tab query params to settings

* Decrease navigation font size

* Change order of providers

* Increase padding for modals

* Show page only when user is fully loaded and found

* Remove unused state

* Remove integrations page
2024-04-02 14:06:38 +02:00
amplitudes 859916b1df fix: user deletion notification (#367) 2024-04-02 12:26:45 +02:00
Eduard Gert 80ce7d21b0 Fix issue where the first users cache is not populated (#366) 2024-03-28 11:27:00 +01:00
Eduard Gert 06fdbd8ec4 Hide profile settings and announcements for blocked dashboard view (#365) 2024-03-28 10:25:21 +01:00
Eduard Gert 973cceff79 Add setting to change dashboard view for regular users (#362) 2024-03-27 16:09:58 +01:00
Eduard Gert f4a2d6fae8 Add Okta SCIM integration (#361)
* Add Okta integration (wip)

* Update okta setup dialog

* Add okta integration images

* Add error handling for 500 status codes

* Add okta integration

* Fix lint warnings

* Update azures last sync time

* Remove 'on' from step, disable copy for HTTP Header

* Update text for custom IDP
2024-03-27 15:55:56 +01:00
Eduard Gert cb922b46b7 Add 'Offline' filter to peers table (#364) 2024-03-26 20:03:24 +01:00
Eduard Gert 4c56ae704c Show peers for regular users but hide / disable actions (delete, enable ssh etc.) (#360)
* Show peers for regular users but hide / disable actions (delete, enable ssh etc.)

* Do not load countries for regular users
2024-03-21 14:21:26 +01:00
64 changed files with 591 additions and 3388 deletions

No files matched your search

+2 -1
View File
@@ -4,6 +4,7 @@ on:
branches:
- "feature/**"
- main
- debug
tags:
- "**"
pull_request:
@@ -55,7 +56,7 @@ jobs:
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
push: true
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+4 -1
View File
@@ -1,4 +1,3 @@
# simple server configuration to replace nginx's default
server {
listen 80 default_server;
listen [::]:80 default_server;
@@ -7,10 +6,14 @@ server {
location / {
try_files $uri $uri.html $uri/ =404;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
}
-3
View File
@@ -5,14 +5,12 @@ import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import useFetchApi from "@utils/api";
import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import ActivityIcon from "@/assets/icons/ActivityIcon";
import { ActivityEvent } from "@/interfaces/ActivityEvent";
import PageContainer from "@/layouts/PageContainer";
import ActivityTable from "@/modules/activity/ActivityTable";
import { EventStreamingCard } from "@/modules/integrations/event-streaming/EventStreamingCard";
export default function Activity() {
const { data: events, isLoading } = useFetchApi<ActivityEvent[]>("/events");
@@ -50,7 +48,6 @@ export default function Activity() {
</Paragraph>
</div>
<RestrictedAccess page={"Activity"}>
{(isLocalDev() || isNetBirdHosted()) && <EventStreamingCard />}
<ActivityTable events={events} isLoading={isLoading} />
</RestrictedAccess>
</PageContainer>
@@ -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;
-39
View File
@@ -1,39 +0,0 @@
"use client";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { VerticalTabs } from "@components/VerticalTabs";
import { FileText, FingerprintIcon } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useState } from "react";
import PageContainer from "@/layouts/PageContainer";
import EventStreamingTab from "@/modules/integrations/event-streaming/EventStreamingTab";
import IdentityProviderTab from "@/modules/integrations/idp-sync/IdentityProviderTab";
export default function Integrations() {
const searchParams = useSearchParams();
const currentTab = searchParams.get("tab");
const [tab, setTab] = useState(currentTab || "event-streaming");
return (
<PageContainer>
<VerticalTabs value={tab} onChange={setTab}>
<VerticalTabs.List>
<VerticalTabs.Trigger value="event-streaming">
<FileText size={14} />
Event Streaming
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="identity-provider">
<FingerprintIcon size={14} />
Identity Provider
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess page={"Integrations"}>
<div className={"border-l border-nb-gray-930 w-full"}>
<EventStreamingTab />
<IdentityProviderTab />
</div>
</RestrictedAccess>
</VerticalTabs>
</PageContainer>
);
}
+114 -53
View File
@@ -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"}>
+8 -4
View File
@@ -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
+23 -3
View File
@@ -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>
+1 -1
View File
@@ -5,7 +5,7 @@ export type IconProps = {
};
export const defaultIconProps: IconProps = {
size: 16,
size: 15,
className:
"dark:fill-nb-gray-400 fill-gray-500 peer-data-[active=true]/icon:dark:fill-white peer-data-[active=true]/icon:fill-gray-900 shrink-0",
autoHeight: false,
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+1 -1
View File
@@ -34,7 +34,7 @@ const auth0AuthorityConfig: AuthorityConfiguration = {
const onEvent = (configurationName: any, eventName: any, data: any) => {
if (process.env.NODE_ENV !== "production") {
//console.info(`oidc:${configurationName}:${eventName}`, data);
console.info(`oidc:${configurationName}:${eventName}`, data);
}
};
+7 -1
View File
@@ -1,4 +1,5 @@
import { OidcSecure, useOidc } from "@axa-fr/react-oidc";
import loadConfig from "@utils/config";
import { usePathname } from "next/navigation";
import * as React from "react";
import { useEffect } from "react";
@@ -6,13 +7,18 @@ import { useEffect } from "react";
type Props = {
children: React.ReactNode;
};
const config = loadConfig();
export const SecureProvider = ({ children }: Props) => {
const { isAuthenticated, login } = useOidc();
const currentPath = usePathname();
useEffect(() => {
console.log("isAuthenticated", isAuthenticated);
if (!isAuthenticated) {
login(currentPath);
console.info("Not authenticated, logging in...");
login(currentPath, { client_id: config.clientId });
}
}, [currentPath, isAuthenticated, login]);
+1 -1
View File
@@ -60,7 +60,7 @@ export default function SidebarItem({
<li className={"px-4 cursor-pointer"}>
<button
className={classNames(
"rounded-lg text-base w-full ",
"rounded-lg text-[.95rem] w-full ",
"font-normal ",
className,
isChild ? "pl-7 pr-2 py-2 mt-1 mb-0.5" : "py-2 px-3",
+5 -2
View File
@@ -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}
/>
);
})}
+18 -13
View File
@@ -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} />
+14 -1
View File
@@ -19,6 +19,7 @@ const AnalyticsContext = React.createContext(
{} as {
initialized: boolean;
trackPageView: () => void;
trackEvent: (category: string, action: string, label: string) => void;
},
);
const config = loadConfig();
@@ -51,8 +52,20 @@ export default function AnalyticsProvider({ children }: Props) {
ReactGA.send({ hitType: "pageview", page: path, title: document.title });
};
const trackEvent = (category: string, action: string, label: string) => {
if (isProduction() && ReactGA.isInitialized) {
ReactGA.event({
category: category,
action: action,
label: label,
});
}
};
return (
<AnalyticsContext.Provider value={{ initialized, trackPageView }}>
<AnalyticsContext.Provider
value={{ initialized, trackPageView, trackEvent }}
>
{children}
</AnalyticsContext.Provider>
);
+16 -3
View File
@@ -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>
+26 -5
View File
@@ -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
+10 -4
View File
@@ -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,
);
+5 -2
View File
@@ -81,10 +81,13 @@ export default function DialogProvider({ children }: Props) {
/>
{dialogOptions.children && (
<div className={"px-8 pt-4"}>{dialogOptions.children}</div>
<div className={"px-8 pt-0"}>{dialogOptions.children}</div>
)}
<ModalFooter className={"items-center gap-2"} separator={false}>
<ModalFooter
className={"items-center gap-2 pt-5"}
separator={false}
>
<ModalClose asChild={true}>
<Button
variant={"secondary"}
Loaded 20 of 64 files, more files were not shown because too many files have changed in this diff. Show more