Compare commits

..
43 changed files with 160 additions and 318 deletions

No files matched your search

+1 -2
View File
@@ -12,6 +12,5 @@
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL"
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID"
}
+7 -22
View File
@@ -14,24 +14,17 @@ import { IconSettings2 } from "@tabler/icons-react";
import useFetchApi, { useApiCall } from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React from "react";
import Skeleton from "react-loading-skeleton";
import { useSWRConfig } from "swr";
import DNSIcon from "@/assets/icons/DNSIcon";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { NameserverSettings } from "@/interfaces/NameserverSettings";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
export default function NameServerSettings() {
const { data: settings, isLoading } =
useFetchApi<NameserverSettings>("/dns/settings");
const initialDNSGroups = useGroupIdsToGroups(
settings?.disabled_management_groups,
);
return (
<PageContainer>
<div className={"p-default py-6"}>
@@ -62,16 +55,10 @@ export default function NameServerSettings() {
in our documentation.
</Paragraph>
<RestrictedAccess page={"DNS Settings"}>
{!isLoading && initialDNSGroups !== undefined ? (
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
) : (
<div>
<Skeleton
width={"100%"}
className={"mt-8 max-w-xl"}
height={240}
/>
</div>
{!isLoading && (
<SettingDisabledManagementGroups
initial={settings?.disabled_management_groups}
/>
)}
</RestrictedAccess>
</div>
@@ -80,16 +67,16 @@ export default function NameServerSettings() {
}
const SettingDisabledManagementGroups = ({
initialGroups,
initial,
}: {
initialGroups: Group[];
initial: string[] | undefined;
}) => {
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
const { mutate } = useSWRConfig();
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initialGroups,
initial: initial || [],
});
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
@@ -121,7 +108,6 @@ const SettingDisabledManagementGroups = ({
Peers in these groups will require manual domain name resolution
</HelpText>
<PeerGroupSelector
dataCy={"dns-groups-selector"}
onChange={setSelectedGroups}
values={selectedGroups}
/>
@@ -136,7 +122,6 @@ const SettingDisabledManagementGroups = ({
size={"sm"}
onClick={saveSettings}
disabled={!hasChanges}
data-cy={"save-changes"}
>
Save Changes
</Button>
+8 -25
View File
@@ -297,29 +297,12 @@ function PeerOverview() {
/>
</FullTooltip>
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<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}
>
{!isUser && (
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<PeerGroupSelector
disabled={isUser}
onChange={setSelectedGroups}
@@ -327,8 +310,8 @@ function PeerOverview() {
hideAllGroup={true}
peer={peer}
/>
</FullTooltip>
</div>
</div>
)}
</div>
</div>
</div>
+1 -5
View File
@@ -11,7 +11,6 @@ import {
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Role } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
@@ -53,10 +52,7 @@ export default function NetBirdSettings() {
Danger zone
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess
page={"Settings"}
allow={[Role.Admin, Role.Owner, Role.BillingAdmin]}
>
<RestrictedAccess page={"Settings"}>
<div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />}
+5 -15
View File
@@ -22,13 +22,11 @@ import { useSWRConfig } from "swr";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import { Group } from "@/interfaces/Group";
import { Role, User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
@@ -47,10 +45,8 @@ export default function UserPage() {
useRedirect("/team/users", false, !userId);
const userGroups = useGroupIdsToGroups(user?.auto_groups);
return !isLoading && user && userGroups !== undefined ? (
<UserOverview user={user} initialGroups={userGroups} />
return !isLoading && user ? (
<UserOverview user={user} />
) : (
<FullScreenLoading />
);
@@ -58,16 +54,16 @@ export default function UserPage() {
type Props = {
user: User;
initialGroups: Group[];
};
function UserOverview({ user, initialGroups }: Readonly<Props>) {
function UserOverview({ user }: Props) {
const router = useRouter();
const userRequest = useApiCall<User>("/users");
const { mutate } = useSWRConfig();
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
const initialGroups = user.auto_groups;
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initialGroups,
@@ -184,7 +180,6 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
className={"w-full"}
disabled={!hasChanges}
onClick={save}
data-cy={"save-changes"}
>
Save Changes
</Button>
@@ -206,7 +201,6 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
onChange={setSelectedGroups}
values={selectedGroups}
hideAllGroup={true}
dataCy={"user-group-selector"}
/>
</div>
)}
@@ -250,10 +244,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
<div className={"inline-flex gap-4 justify-end"}>
<div>
<CreateAccessTokenModal user={user}>
<Button
variant={"primary"}
data-cy={"access-token-open-modal"}
>
<Button variant={"primary"}>
<IconCirclePlus size={16} />
Create Access Token
</Button>
@@ -302,7 +293,6 @@ function UserInformationCard({ user }: { user: User }) {
)}
<Card.ListItem
tooltip={false}
label={
<>
<GalleryHorizontalEnd size={16} />
+1 -3
View File
@@ -10,14 +10,12 @@ import useFetchApi from "@utils/api";
import { ExternalLinkIcon, User2 } from "lucide-react";
import React, { lazy, Suspense } from "react";
import TeamIcon from "@/assets/icons/TeamIcon";
import { useGroups } from "@/contexts/GroupsProvider";
import { User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
export default function TeamUsers() {
const { isLoading: isGroupsLoading } = useGroups();
const { data: users, isLoading } = useFetchApi<User[]>(
"/users?service_user=false",
);
@@ -62,7 +60,7 @@ export default function TeamUsers() {
<Suspense fallback={<SkeletonTable />}>
<UsersTable
users={users}
isLoading={isLoading || isGroupsLoading}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense>
+1
View File
@@ -13,6 +13,7 @@ function NetBirdIcon({ size = 16, className }: Props) {
src={NetBirdLogo}
alt={"Netbird Icon"}
width={size}
priority={true}
className={className}
/>
);
+1 -1
View File
@@ -20,7 +20,7 @@ export const DropdownInput = forwardRef<HTMLInputElement, Props>(
"min-h-[42px] w-full relative",
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10 select-none",
)}
value={value}
onChange={(e) => onChange(e.target.value)}
+36 -13
View File
@@ -11,7 +11,7 @@ import useSortedDropdownOptions from "@hooks/useSortedDropdownOptions";
import { IconArrowBack } from "@tabler/icons-react";
import { cn } from "@utils/helpers";
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
import { sortBy, trim, unionBy } from "lodash";
import { sortBy, trim, unionBy, uniqBy } from "lodash";
import {
ChevronsUpDown,
FolderGit2,
@@ -55,8 +55,13 @@ export function PeerGroupSelector({
disabledGroups,
dataCy = "group-selector-dropdown",
}: Readonly<MultiSelectProps>) {
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
useGroups();
const {
groups,
dropdownOptions,
setDropdownOptions,
addDropdownOptions,
updateGroupDropdown,
} = useGroups();
const searchRef = React.useRef<HTMLInputElement>(null);
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
const [search, setSearch] = useState("");
@@ -93,10 +98,18 @@ export function PeerGroupSelector({
const selectGroup = (name: string) => {
const group = groups?.find((group) => group.name == name);
const option = dropdownOptions.find((option) => option.name == name);
const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || [];
let groupPeers: GroupPeer[] = [...((group?.peers as GroupPeer[]) || [])];
if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (peer) {
groupPeers.push({ id: peer.id as string, name: peer.name });
groupPeers = uniqBy(groupPeers, "id");
updateGroupDropdown(name, {
...group,
name: name,
peers: groupPeers,
peers_count: groupPeers.length,
});
}
if (!group && !option) {
addDropdownOptions([{ name: name, peers: groupPeers }]);
@@ -117,6 +130,18 @@ export function PeerGroupSelector({
// Remove group from the groupOptions if it does not have an id
const deselectGroup = (name: string) => {
onChange((previous) => {
if (peer) {
const option = dropdownOptions?.find((group) => group.name === name);
const groupPeers: GroupPeer[] = [
...((option?.peers as GroupPeer[]) || []),
].filter((p) => p?.id !== peer?.id);
updateGroupDropdown(name, {
...option,
name,
peers: groupPeers,
peers_count: groupPeers.length,
});
}
return previous.filter((group) => group.name != name);
});
};
@@ -162,6 +187,7 @@ export function PeerGroupSelector({
dropdownOptions,
values,
open,
hideAllGroup,
);
return (
@@ -241,7 +267,7 @@ export function PeerGroupSelector({
)}
</div>
<div className={"pl-2"} data-cy={"group-selector-open-close"}>
<div className={"pl-2"}>
<ChevronsUpDown
size={18}
className={"shrink-0 group-hover:text-nb-gray-300 transition-all"}
@@ -276,7 +302,7 @@ export function PeerGroupSelector({
"min-h-[42px] w-full relative",
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10 select-none",
)}
ref={searchRef}
value={search}
@@ -311,10 +337,7 @@ export function PeerGroupSelector({
<CommandGroup>
<ScrollArea
className={cn(
"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3",
sortedDropdownOptions.length == 0 && !search && "py-0",
)}
className={"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3"}
>
{searchedGroupNotFound && (
<CommandItem
@@ -387,7 +410,7 @@ export function PeerGroupSelector({
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2 select-none"
}
>
{peerIcon}
+2 -2
View File
@@ -166,7 +166,7 @@ export function PeerSelector({
placeholder={"Search for peers by name or ip..."}
/>
{unfilteredItems.length == 0 && (
{unfilteredItems.length == 0 && search == "" && (
<DropdownInfoText>
{
"Seems like you don't have any linux peers to assign as a routing peer."
@@ -174,7 +174,7 @@ export function PeerSelector({
</DropdownInfoText>
)}
{filteredItems.length == 0 && (
{filteredItems.length == 0 && search != "" && (
<DropdownInfoText>
There are no peers matching your search.
</DropdownInfoText>
@@ -4,7 +4,7 @@ export const GradientFadedBackground = () => {
return (
<div
className={
"h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0 pointer-events-none"
"h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0"
}
>
<div
-1
View File
@@ -27,7 +27,6 @@ export default function GroupBadge({
<Badge
key={group.id || group.name}
useHover={true}
data-cy={"group-badge"}
variant={"gray-ghost"}
className={cn("transition-all group whitespace-nowrap", className)}
onClick={(e) => {
+1 -4
View File
@@ -34,10 +34,7 @@ export default function MultipleGroups({
<TooltipProvider disableHoverableContent={false}>
<Tooltip delayDuration={1}>
<TooltipTrigger asChild={true}>
<div
className={"inline-flex items-center gap-2 z-0"}
data-cy={"multiple-groups"}
>
<div className={"inline-flex items-center gap-2 z-0"}>
{firstGroup && <GroupBadge group={firstGroup} />}
{otherGroups && otherGroups.length > 0 && (
<Badge
+2 -1
View File
@@ -34,8 +34,9 @@ export default function UserDropdown() {
};
useHotkeys("shift+mod+l", () => logout(), []);
const { isOwnerOrAdmin, permission, isBillingAdmin } = useLoggedInUser();
const [dropdownOpen, setDropdownOpen] = useState(false);
const { permission } = useLoggedInUser();
return (
<DropdownMenu
+16 -4
View File
@@ -24,17 +24,29 @@ const GroupContext = React.createContext(
);
export default function GroupsProvider({ children }: Props) {
const { permission } = useLoggedInUser();
const { permission, isUser } = useLoggedInUser();
return permission.dashboard_view == "blocked" ? (
<>{children}</>
) : (
<GroupsProviderContent>{children}</GroupsProviderContent>
<GroupsProviderContent isUser={isUser}>{children}</GroupsProviderContent>
);
}
export function GroupsProviderContent({ children }: Props) {
const { data: groups, mutate, isLoading } = useFetchApi<Group[]>("/groups");
type ProviderContentProps = {
children: React.ReactNode;
isUser: boolean;
};
export function GroupsProviderContent({
children,
isUser,
}: Readonly<ProviderContentProps>) {
const {
data: groups,
mutate,
isLoading,
} = useFetchApi<Group[]>("/groups", false, true, !isUser);
const groupRequest = useApiCall<Group>("/groups", true);
const [dropdownOptions, setDropdownOptions] = useState<Group[]>([]);
+4 -7
View File
@@ -20,7 +20,6 @@ const RoutesContext = React.createContext(
toUpdate: Partial<Route>,
onSuccess?: (route: Route) => void,
message?: string,
options?: { remove_access_control_groups?: boolean },
) => void;
},
);
@@ -34,7 +33,6 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
toUpdate: Partial<Route>,
onSuccess?: (route: Route) => void,
message?: string,
options?: { remove_access_control_groups?: boolean },
) => {
const hasDomains = route.domains ? route.domains.length > 0 : false;
@@ -56,11 +54,10 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
metric: toUpdate.metric ?? route.metric ?? 9999,
masquerade: toUpdate.masquerade ?? route.masquerade ?? true,
groups: toUpdate.groups ?? route.groups ?? [],
access_control_groups: options?.remove_access_control_groups
? undefined
: toUpdate.access_control_groups ??
route.access_control_groups ??
undefined,
access_control_groups:
toUpdate.access_control_groups ??
route.access_control_groups ??
undefined,
},
`/${route.id}`,
)
+3 -10
View File
@@ -1,9 +1,8 @@
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useFetchApi from "@utils/api";
import { isNetBirdHosted } from "@utils/netbird";
import React, { useMemo } from "react";
import { Permission } from "@/interfaces/Permission";
import { Role, User } from "@/interfaces/User";
import { User } from "@/interfaces/User";
type Props = {
children: React.ReactNode;
@@ -41,13 +40,8 @@ export const useUsers = () => React.useContext(UsersContext);
export const useLoggedInUser = () => {
const { loggedInUser } = useUsers();
const isOwner = loggedInUser ? loggedInUser?.role === Role.Owner : false;
const isAdmin = loggedInUser ? loggedInUser?.role === Role.Admin : false;
const isBillingAdmin = isNetBirdHosted()
? loggedInUser
? loggedInUser?.role === Role.BillingAdmin
: false
: false;
const isOwner = loggedInUser ? loggedInUser?.role === "owner" : false;
const isAdmin = loggedInUser ? loggedInUser?.role === "admin" : false;
const isUser = !isOwner && !isAdmin;
const isOwnerOrAdmin = isOwner || isAdmin;
@@ -62,7 +56,6 @@ export const useLoggedInUser = () => {
isOwner,
isAdmin,
isUser,
isBillingAdmin,
isOwnerOrAdmin,
permission,
} as const;
+10 -6
View File
@@ -5,6 +5,7 @@ const useSortedDropdownOptions = (
dropdownOptions: Group[],
values: Group[],
isPopupOpen: boolean,
hideAllGroupOption = false,
): Group[] => {
const sortOrderRef = useRef<Map<string, number>>(new Map());
const prevValuesRef = useRef<Group[]>([]);
@@ -25,12 +26,15 @@ const useSortedDropdownOptions = (
// Sort the dropdown options based on the current sort order
return useMemo(() => {
const sortOrder = sortOrderRef.current;
return [...dropdownOptions].sort((a, b) => {
const indexA = sortOrder.get(a.name) ?? Infinity;
const indexB = sortOrder.get(b.name) ?? Infinity;
return indexA - indexB;
});
}, [dropdownOptions, sortOrderRef.current]);
return [...dropdownOptions]
.sort((a, b) => {
const indexA = sortOrder.get(a.name) ?? Infinity;
const indexB = sortOrder.get(b.name) ?? Infinity;
if (a.name === "All") return -1; // Move "All" to the top
return indexA - indexB;
})
.filter((group) => !hideAllGroupOption || group.name !== "All");
}, [dropdownOptions, sortOrderRef.current, hideAllGroupOption]);
};
export default useSortedDropdownOptions;
-1
View File
@@ -35,5 +35,4 @@ export interface GroupedRoute {
description?: string;
description_search?: string;
domain_search?: string;
routes_search?: string;
}
-1
View File
@@ -18,5 +18,4 @@ export enum Role {
User = "user",
Admin = "admin",
Owner = "owner",
BillingAdmin = "billing_admin",
}
Loaded 20 of 43 files, more files were not shown because too many files have changed in this diff. Show more