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
35 changed files with 252 additions and 67 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>
+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>
+5 -2
View File
@@ -241,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"}
@@ -311,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
@@ -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
+1 -3
View File
@@ -1,6 +1,5 @@
import useFetchApi, { useApiCall } from "@utils/api";
import { merge, sortBy, unionBy } from "lodash";
import { usePathname } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Group } from "@/interfaces/Group";
@@ -25,10 +24,9 @@ const GroupContext = React.createContext(
);
export default function GroupsProvider({ children }: Props) {
const path = usePathname();
const { permission } = useLoggedInUser();
return path === "/peers" && permission.dashboard_view == "blocked" ? (
return permission.dashboard_view == "blocked" ? (
<>{children}</>
) : (
<GroupsProviderContent>{children}</GroupsProviderContent>
+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;
+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",
}
+1
View File
@@ -11,6 +11,7 @@ export default function PageContainer({ children, className }: Props) {
className={cn(
className,
"relative flex-auto overflow-auto bg-nb-gray z-1",
"focus:outline-none",
)}
>
{children}
@@ -46,7 +46,12 @@ export default function AccessTokenActionCell({ access_token }: Props) {
return (
<div className={"flex justify-end pr-4"}>
<Button variant={"danger-outline"} size={"sm"} onClick={handleConfirm}>
<Button
variant={"danger-outline"}
size={"sm"}
onClick={handleConfirm}
data-cy={"access-token-delete"}
>
<Trash2 size={16} />
Delete
</Button>
@@ -98,6 +98,7 @@ export default function CreateAccessTokenModal({ children, user }: Props) {
variant={"secondary"}
className={"w-full"}
tabIndex={-1}
data-cy={"access-token-copy-close"}
>
Close
</Button>
@@ -170,6 +171,7 @@ export function AccessTokenModalContent({ onSuccess, user }: ModalProps) {
<Label>Name</Label>
<HelpText>Set an easily identifiable name for your token</HelpText>
<Input
data-cy={"access-token-name"}
placeholder={"e.g., Infra token"}
value={name}
onChange={(e) => setName(e.target.value)}
@@ -184,6 +186,7 @@ export function AccessTokenModalContent({ onSuccess, user }: ModalProps) {
<Input
maxWidthClass={"max-w-[200px]"}
placeholder={"30"}
data-cy={"access-token-expires-in"}
min={1}
max={365}
value={expiresIn}
@@ -215,7 +218,12 @@ export function AccessTokenModalContent({ onSuccess, user }: ModalProps) {
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button variant={"primary"} onClick={submit} disabled={isDisabled}>
<Button
variant={"primary"}
onClick={submit}
disabled={isDisabled}
data-cy={"create-access-token"}
>
<PlusCircle size={16} />
Create Token
</Button>
+8 -1
View File
@@ -1,9 +1,16 @@
import useFetchApi from "@utils/api";
import { useMemo } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Account } from "@/interfaces/Account";
export const useAccount = () => {
const { data: accounts } = useFetchApi<Account[]>("/accounts");
const { isBillingAdmin } = useLoggedInUser();
const { data: accounts } = useFetchApi<Account[]>(
"/accounts",
true,
true,
!isBillingAdmin,
);
return useMemo(() => {
if (!accounts) return;
@@ -0,0 +1,25 @@
import { uniq } from "lodash";
import { useEffect, useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import type { Group } from "@/interfaces/Group";
export const useGroupIdsToGroups = (initial?: string[]) => {
const { groups, isLoading } = useGroups();
const [initialSet, setInitialSet] = useState(false);
const [mappedGroups, setMappedGroups] = useState<Group[] | undefined>(
undefined,
);
useEffect(() => {
// Only run the mapping once when groups are loaded and initial IDs are available
if (!initialSet && !isLoading && groups && initial) {
const mapped = uniq(initial)
.map((group) => groups.find((g) => g?.id === group))
.filter((g): g is Group => g !== undefined);
setMappedGroups(mapped);
setInitialSet(true); // Mark that we've done the initial mapping to prevent subsequent runs
}
}, [groups, initial, isLoading, initialSet]);
return useMemo(() => mappedGroups, [mappedGroups]);
};
Loaded 20 of 35 files, more files were not shown because too many files have changed in this diff. Show more