Compare commits

..
Author SHA1 Message Date
Eduard Gert e167afb06b Fix missing groups on users and dns settings page 2024-11-13 16:18:54 +01:00
43 changed files with 318 additions and 160 deletions

No files matched your search

+2 -1
View File
@@ -12,5 +12,6 @@
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID"
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL"
}
+22 -7
View File
@@ -14,17 +14,24 @@ 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"}>
@@ -55,10 +62,16 @@ export default function NameServerSettings() {
in our documentation.
</Paragraph>
<RestrictedAccess page={"DNS Settings"}>
{!isLoading && (
<SettingDisabledManagementGroups
initial={settings?.disabled_management_groups}
/>
{!isLoading && initialDNSGroups !== undefined ? (
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
) : (
<div>
<Skeleton
width={"100%"}
className={"mt-8 max-w-xl"}
height={240}
/>
</div>
)}
</RestrictedAccess>
</div>
@@ -67,16 +80,16 @@ export default function NameServerSettings() {
}
const SettingDisabledManagementGroups = ({
initial,
initialGroups,
}: {
initial: string[] | undefined;
initialGroups: Group[];
}) => {
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
const { mutate } = useSWRConfig();
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
useGroupHelper({
initial: initial || [],
initial: initialGroups,
});
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
@@ -108,6 +121,7 @@ const SettingDisabledManagementGroups = ({
Peers in these groups will require manual domain name resolution
</HelpText>
<PeerGroupSelector
dataCy={"dns-groups-selector"}
onChange={setSelectedGroups}
values={selectedGroups}
/>
@@ -122,6 +136,7 @@ const SettingDisabledManagementGroups = ({
size={"sm"}
onClick={saveSettings}
disabled={!hasChanges}
data-cy={"save-changes"}
>
Save Changes
</Button>
+25 -8
View File
@@ -297,12 +297,29 @@ function PeerOverview() {
/>
</FullTooltip>
{!isUser && (
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<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}
>
<PeerGroupSelector
disabled={isUser}
onChange={setSelectedGroups}
@@ -310,8 +327,8 @@ function PeerOverview() {
hideAllGroup={true}
peer={peer}
/>
</div>
)}
</FullTooltip>
</div>
</div>
</div>
</div>
+5 -1
View File
@@ -11,6 +11,7 @@ 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";
@@ -52,7 +53,10 @@ export default function NetBirdSettings() {
Danger zone
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess page={"Settings"}>
<RestrictedAccess
page={"Settings"}
allow={[Role.Admin, Role.Owner, Role.BillingAdmin]}
>
<div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />}
+15 -5
View File
@@ -22,11 +22,13 @@ 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";
@@ -45,8 +47,10 @@ export default function UserPage() {
useRedirect("/team/users", false, !userId);
return !isLoading && user ? (
<UserOverview user={user} />
const userGroups = useGroupIdsToGroups(user?.auto_groups);
return !isLoading && user && userGroups !== undefined ? (
<UserOverview user={user} initialGroups={userGroups} />
) : (
<FullScreenLoading />
);
@@ -54,16 +58,16 @@ export default function UserPage() {
type Props = {
user: User;
initialGroups: Group[];
};
function UserOverview({ user }: Props) {
function UserOverview({ user, initialGroups }: Readonly<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,
@@ -180,6 +184,7 @@ function UserOverview({ user }: Props) {
className={"w-full"}
disabled={!hasChanges}
onClick={save}
data-cy={"save-changes"}
>
Save Changes
</Button>
@@ -201,6 +206,7 @@ function UserOverview({ user }: Props) {
onChange={setSelectedGroups}
values={selectedGroups}
hideAllGroup={true}
dataCy={"user-group-selector"}
/>
</div>
)}
@@ -244,7 +250,10 @@ function UserOverview({ user }: Props) {
<div className={"inline-flex gap-4 justify-end"}>
<div>
<CreateAccessTokenModal user={user}>
<Button variant={"primary"}>
<Button
variant={"primary"}
data-cy={"access-token-open-modal"}
>
<IconCirclePlus size={16} />
Create Access Token
</Button>
@@ -293,6 +302,7 @@ function UserInformationCard({ user }: { user: User }) {
)}
<Card.ListItem
tooltip={false}
label={
<>
<GalleryHorizontalEnd size={16} />
+3 -1
View File
@@ -10,12 +10,14 @@ 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",
);
@@ -60,7 +62,7 @@ export default function TeamUsers() {
<Suspense fallback={<SkeletonTable />}>
<UsersTable
users={users}
isLoading={isLoading}
isLoading={isLoading || isGroupsLoading}
headingTarget={portalTarget}
/>
</Suspense>
-1
View File
@@ -13,7 +13,6 @@ 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 select-none",
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
)}
value={value}
onChange={(e) => onChange(e.target.value)}
+13 -36
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, uniqBy } from "lodash";
import { sortBy, trim, unionBy } from "lodash";
import {
ChevronsUpDown,
FolderGit2,
@@ -55,13 +55,8 @@ export function PeerGroupSelector({
disabledGroups,
dataCy = "group-selector-dropdown",
}: Readonly<MultiSelectProps>) {
const {
groups,
dropdownOptions,
setDropdownOptions,
addDropdownOptions,
updateGroupDropdown,
} = useGroups();
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
useGroups();
const searchRef = React.useRef<HTMLInputElement>(null);
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
const [search, setSearch] = useState("");
@@ -98,18 +93,10 @@ export function PeerGroupSelector({
const selectGroup = (name: string) => {
const group = groups?.find((group) => group.name == name);
const option = dropdownOptions.find((option) => option.name == name);
let groupPeers: GroupPeer[] = [...((group?.peers as GroupPeer[]) || [])];
const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || [];
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 (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (!group && !option) {
addDropdownOptions([{ name: name, peers: groupPeers }]);
@@ -130,18 +117,6 @@ 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);
});
};
@@ -187,7 +162,6 @@ export function PeerGroupSelector({
dropdownOptions,
values,
open,
hideAllGroup,
);
return (
@@ -267,7 +241,7 @@ export function PeerGroupSelector({
)}
</div>
<div className={"pl-2"}>
<div className={"pl-2"} data-cy={"group-selector-open-close"}>
<ChevronsUpDown
size={18}
className={"shrink-0 group-hover:text-nb-gray-300 transition-all"}
@@ -302,7 +276,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 select-none",
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
)}
ref={searchRef}
value={search}
@@ -337,7 +311,10 @@ export function PeerGroupSelector({
<CommandGroup>
<ScrollArea
className={"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3"}
className={cn(
"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3",
sortedDropdownOptions.length == 0 && !search && "py-0",
)}
>
{searchedGroupNotFound && (
<CommandItem
@@ -410,7 +387,7 @@ export function PeerGroupSelector({
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2 select-none"
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
}
>
{peerIcon}
+2 -2
View File
@@ -166,7 +166,7 @@ export function PeerSelector({
placeholder={"Search for peers by name or ip..."}
/>
{unfilteredItems.length == 0 && search == "" && (
{unfilteredItems.length == 0 && (
<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 && search != "" && (
{filteredItems.length == 0 && (
<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"
"h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0 pointer-events-none"
}
>
<div
+1
View File
@@ -27,6 +27,7 @@ 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) => {
+4 -1
View File
@@ -34,7 +34,10 @@ export default function MultipleGroups({
<TooltipProvider disableHoverableContent={false}>
<Tooltip delayDuration={1}>
<TooltipTrigger asChild={true}>
<div className={"inline-flex items-center gap-2 z-0"}>
<div
className={"inline-flex items-center gap-2 z-0"}
data-cy={"multiple-groups"}
>
{firstGroup && <GroupBadge group={firstGroup} />}
{otherGroups && otherGroups.length > 0 && (
<Badge
+1 -2
View File
@@ -34,9 +34,8 @@ export default function UserDropdown() {
};
useHotkeys("shift+mod+l", () => logout(), []);
const { isOwnerOrAdmin, permission, isBillingAdmin } = useLoggedInUser();
const [dropdownOpen, setDropdownOpen] = useState(false);
const { permission } = useLoggedInUser();
return (
<DropdownMenu
+4 -16
View File
@@ -24,29 +24,17 @@ const GroupContext = React.createContext(
);
export default function GroupsProvider({ children }: Props) {
const { permission, isUser } = useLoggedInUser();
const { permission } = useLoggedInUser();
return permission.dashboard_view == "blocked" ? (
<>{children}</>
) : (
<GroupsProviderContent isUser={isUser}>{children}</GroupsProviderContent>
<GroupsProviderContent>{children}</GroupsProviderContent>
);
}
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);
export function GroupsProviderContent({ children }: Props) {
const { data: groups, mutate, isLoading } = useFetchApi<Group[]>("/groups");
const groupRequest = useApiCall<Group>("/groups", true);
const [dropdownOptions, setDropdownOptions] = useState<Group[]>([]);
+7 -4
View File
@@ -20,6 +20,7 @@ const RoutesContext = React.createContext(
toUpdate: Partial<Route>,
onSuccess?: (route: Route) => void,
message?: string,
options?: { remove_access_control_groups?: boolean },
) => void;
},
);
@@ -33,6 +34,7 @@ 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;
@@ -54,10 +56,11 @@ 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:
toUpdate.access_control_groups ??
route.access_control_groups ??
undefined,
access_control_groups: options?.remove_access_control_groups
? undefined
: toUpdate.access_control_groups ??
route.access_control_groups ??
undefined,
},
`/${route.id}`,
)
+10 -3
View File
@@ -1,8 +1,9 @@
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 { User } from "@/interfaces/User";
import { Role, User } from "@/interfaces/User";
type Props = {
children: React.ReactNode;
@@ -40,8 +41,13 @@ export const useUsers = () => React.useContext(UsersContext);
export const useLoggedInUser = () => {
const { loggedInUser } = useUsers();
const isOwner = loggedInUser ? loggedInUser?.role === "owner" : false;
const isAdmin = loggedInUser ? loggedInUser?.role === "admin" : false;
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 isUser = !isOwner && !isAdmin;
const isOwnerOrAdmin = isOwner || isAdmin;
@@ -56,6 +62,7 @@ export const useLoggedInUser = () => {
isOwner,
isAdmin,
isUser,
isBillingAdmin,
isOwnerOrAdmin,
permission,
} as const;
+6 -10
View File
@@ -5,7 +5,6 @@ const useSortedDropdownOptions = (
dropdownOptions: Group[],
values: Group[],
isPopupOpen: boolean,
hideAllGroupOption = false,
): Group[] => {
const sortOrderRef = useRef<Map<string, number>>(new Map());
const prevValuesRef = useRef<Group[]>([]);
@@ -26,15 +25,12 @@ 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;
if (a.name === "All") return -1; // Move "All" to the top
return indexA - indexB;
})
.filter((group) => !hideAllGroupOption || group.name !== "All");
}, [dropdownOptions, sortOrderRef.current, hideAllGroupOption]);
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]);
};
export default useSortedDropdownOptions;
+1
View File
@@ -35,4 +35,5 @@ export interface GroupedRoute {
description?: string;
description_search?: string;
domain_search?: string;
routes_search?: string;
}
+1
View File
@@ -18,4 +18,5 @@ 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