mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e167afb06b |
No files matched your search
+2
-1
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -35,4 +35,5 @@ export interface GroupedRoute {
|
||||
description?: string;
|
||||
description_search?: string;
|
||||
domain_search?: string;
|
||||
routes_search?: string;
|
||||
}
|
||||
@@ -18,4 +18,5 @@ export enum Role {
|
||||
User = "user",
|
||||
Admin = "admin",
|
||||
Owner = "owner",
|
||||
BillingAdmin = "billing_admin",
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Loaded 20 of 37 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user