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",
|
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
||||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
||||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
"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 useFetchApi, { useApiCall } from "@utils/api";
|
||||||
import { ExternalLinkIcon } from "lucide-react";
|
import { ExternalLinkIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Skeleton from "react-loading-skeleton";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||||
|
import { Group } from "@/interfaces/Group";
|
||||||
import { NameserverSettings } from "@/interfaces/NameserverSettings";
|
import { NameserverSettings } from "@/interfaces/NameserverSettings";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||||
|
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
|
||||||
|
|
||||||
export default function NameServerSettings() {
|
export default function NameServerSettings() {
|
||||||
const { data: settings, isLoading } =
|
const { data: settings, isLoading } =
|
||||||
useFetchApi<NameserverSettings>("/dns/settings");
|
useFetchApi<NameserverSettings>("/dns/settings");
|
||||||
|
|
||||||
|
const initialDNSGroups = useGroupIdsToGroups(
|
||||||
|
settings?.disabled_management_groups,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className={"p-default py-6"}>
|
<div className={"p-default py-6"}>
|
||||||
@@ -55,10 +62,16 @@ export default function NameServerSettings() {
|
|||||||
in our documentation.
|
in our documentation.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<RestrictedAccess page={"DNS Settings"}>
|
<RestrictedAccess page={"DNS Settings"}>
|
||||||
{!isLoading && (
|
{!isLoading && initialDNSGroups !== undefined ? (
|
||||||
<SettingDisabledManagementGroups
|
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
|
||||||
initial={settings?.disabled_management_groups}
|
) : (
|
||||||
/>
|
<div>
|
||||||
|
<Skeleton
|
||||||
|
width={"100%"}
|
||||||
|
className={"mt-8 max-w-xl"}
|
||||||
|
height={240}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,16 +80,16 @@ export default function NameServerSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SettingDisabledManagementGroups = ({
|
const SettingDisabledManagementGroups = ({
|
||||||
initial,
|
initialGroups,
|
||||||
}: {
|
}: {
|
||||||
initial: string[] | undefined;
|
initialGroups: Group[];
|
||||||
}) => {
|
}) => {
|
||||||
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
|
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
||||||
useGroupHelper({
|
useGroupHelper({
|
||||||
initial: initial || [],
|
initial: initialGroups,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||||
@@ -108,6 +121,7 @@ const SettingDisabledManagementGroups = ({
|
|||||||
Peers in these groups will require manual domain name resolution
|
Peers in these groups will require manual domain name resolution
|
||||||
</HelpText>
|
</HelpText>
|
||||||
<PeerGroupSelector
|
<PeerGroupSelector
|
||||||
|
dataCy={"dns-groups-selector"}
|
||||||
onChange={setSelectedGroups}
|
onChange={setSelectedGroups}
|
||||||
values={selectedGroups}
|
values={selectedGroups}
|
||||||
/>
|
/>
|
||||||
@@ -122,6 +136,7 @@ const SettingDisabledManagementGroups = ({
|
|||||||
size={"sm"}
|
size={"sm"}
|
||||||
onClick={saveSettings}
|
onClick={saveSettings}
|
||||||
disabled={!hasChanges}
|
disabled={!hasChanges}
|
||||||
|
data-cy={"save-changes"}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||||
|
import { Role } from "@/interfaces/User";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
import { useAccount } from "@/modules/account/useAccount";
|
import { useAccount } from "@/modules/account/useAccount";
|
||||||
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
||||||
@@ -52,7 +53,10 @@ export default function NetBirdSettings() {
|
|||||||
Danger zone
|
Danger zone
|
||||||
</VerticalTabs.Trigger>
|
</VerticalTabs.Trigger>
|
||||||
</VerticalTabs.List>
|
</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"}>
|
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||||
{account && <AuthenticationTab account={account} />}
|
{account && <AuthenticationTab account={account} />}
|
||||||
{account && <PermissionsTab account={account} />}
|
{account && <PermissionsTab account={account} />}
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ import { useSWRConfig } from "swr";
|
|||||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||||
|
import { Group } from "@/interfaces/Group";
|
||||||
import { Role, User } from "@/interfaces/User";
|
import { Role, User } from "@/interfaces/User";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
|
import AccessTokensTable from "@/modules/access-tokens/AccessTokensTable";
|
||||||
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
|
import CreateAccessTokenModal from "@/modules/access-tokens/CreateAccessTokenModal";
|
||||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||||
|
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
|
||||||
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
|
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
|
||||||
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
|
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
|
||||||
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
|
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
|
||||||
@@ -45,8 +47,10 @@ export default function UserPage() {
|
|||||||
|
|
||||||
useRedirect("/team/users", false, !userId);
|
useRedirect("/team/users", false, !userId);
|
||||||
|
|
||||||
return !isLoading && user ? (
|
const userGroups = useGroupIdsToGroups(user?.auto_groups);
|
||||||
<UserOverview user={user} />
|
|
||||||
|
return !isLoading && user && userGroups !== undefined ? (
|
||||||
|
<UserOverview user={user} initialGroups={userGroups} />
|
||||||
) : (
|
) : (
|
||||||
<FullScreenLoading />
|
<FullScreenLoading />
|
||||||
);
|
);
|
||||||
@@ -54,16 +58,16 @@ export default function UserPage() {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User;
|
user: User;
|
||||||
|
initialGroups: Group[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserOverview({ user }: Props) {
|
function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userRequest = useApiCall<User>("/users");
|
const userRequest = useApiCall<User>("/users");
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
|
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
|
||||||
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
|
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
|
||||||
|
|
||||||
const initialGroups = user.auto_groups;
|
|
||||||
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
||||||
useGroupHelper({
|
useGroupHelper({
|
||||||
initial: initialGroups,
|
initial: initialGroups,
|
||||||
@@ -180,6 +184,7 @@ function UserOverview({ user }: Props) {
|
|||||||
className={"w-full"}
|
className={"w-full"}
|
||||||
disabled={!hasChanges}
|
disabled={!hasChanges}
|
||||||
onClick={save}
|
onClick={save}
|
||||||
|
data-cy={"save-changes"}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
@@ -201,6 +206,7 @@ function UserOverview({ user }: Props) {
|
|||||||
onChange={setSelectedGroups}
|
onChange={setSelectedGroups}
|
||||||
values={selectedGroups}
|
values={selectedGroups}
|
||||||
hideAllGroup={true}
|
hideAllGroup={true}
|
||||||
|
dataCy={"user-group-selector"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -244,7 +250,10 @@ function UserOverview({ user }: Props) {
|
|||||||
<div className={"inline-flex gap-4 justify-end"}>
|
<div className={"inline-flex gap-4 justify-end"}>
|
||||||
<div>
|
<div>
|
||||||
<CreateAccessTokenModal user={user}>
|
<CreateAccessTokenModal user={user}>
|
||||||
<Button variant={"primary"}>
|
<Button
|
||||||
|
variant={"primary"}
|
||||||
|
data-cy={"access-token-open-modal"}
|
||||||
|
>
|
||||||
<IconCirclePlus size={16} />
|
<IconCirclePlus size={16} />
|
||||||
Create Access Token
|
Create Access Token
|
||||||
</Button>
|
</Button>
|
||||||
@@ -293,6 +302,7 @@ function UserInformationCard({ user }: { user: User }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Card.ListItem
|
<Card.ListItem
|
||||||
|
tooltip={false}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<GalleryHorizontalEnd size={16} />
|
<GalleryHorizontalEnd size={16} />
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import useFetchApi from "@utils/api";
|
|||||||
import { ExternalLinkIcon, User2 } from "lucide-react";
|
import { ExternalLinkIcon, User2 } from "lucide-react";
|
||||||
import React, { lazy, Suspense } from "react";
|
import React, { lazy, Suspense } from "react";
|
||||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||||
|
import { useGroups } from "@/contexts/GroupsProvider";
|
||||||
import { User } from "@/interfaces/User";
|
import { User } from "@/interfaces/User";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
|
|
||||||
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
|
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
|
||||||
|
|
||||||
export default function TeamUsers() {
|
export default function TeamUsers() {
|
||||||
|
const { isLoading: isGroupsLoading } = useGroups();
|
||||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||||
"/users?service_user=false",
|
"/users?service_user=false",
|
||||||
);
|
);
|
||||||
@@ -60,7 +62,7 @@ export default function TeamUsers() {
|
|||||||
<Suspense fallback={<SkeletonTable />}>
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
<UsersTable
|
<UsersTable
|
||||||
users={users}
|
users={users}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || isGroupsLoading}
|
||||||
headingTarget={portalTarget}
|
headingTarget={portalTarget}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export function PeerGroupSelector({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={"pl-2"}>
|
<div className={"pl-2"} data-cy={"group-selector-open-close"}>
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
size={18}
|
size={18}
|
||||||
className={"shrink-0 group-hover:text-nb-gray-300 transition-all"}
|
className={"shrink-0 group-hover:text-nb-gray-300 transition-all"}
|
||||||
@@ -311,7 +311,10 @@ export function PeerGroupSelector({
|
|||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<ScrollArea
|
<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 && (
|
{searchedGroupNotFound && (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const GradientFadedBackground = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
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
|
<div
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function GroupBadge({
|
|||||||
<Badge
|
<Badge
|
||||||
key={group.id || group.name}
|
key={group.id || group.name}
|
||||||
useHover={true}
|
useHover={true}
|
||||||
|
data-cy={"group-badge"}
|
||||||
variant={"gray-ghost"}
|
variant={"gray-ghost"}
|
||||||
className={cn("transition-all group whitespace-nowrap", className)}
|
className={cn("transition-all group whitespace-nowrap", className)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export default function MultipleGroups({
|
|||||||
<TooltipProvider disableHoverableContent={false}>
|
<TooltipProvider disableHoverableContent={false}>
|
||||||
<Tooltip delayDuration={1}>
|
<Tooltip delayDuration={1}>
|
||||||
<TooltipTrigger asChild={true}>
|
<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} />}
|
{firstGroup && <GroupBadge group={firstGroup} />}
|
||||||
{otherGroups && otherGroups.length > 0 && (
|
{otherGroups && otherGroups.length > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ export default function UserDropdown() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys("shift+mod+l", () => logout(), []);
|
useHotkeys("shift+mod+l", () => logout(), []);
|
||||||
|
const { isOwnerOrAdmin, permission, isBillingAdmin } = useLoggedInUser();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const { permission } = useLoggedInUser();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import useFetchApi, { useApiCall } from "@utils/api";
|
import useFetchApi, { useApiCall } from "@utils/api";
|
||||||
import { merge, sortBy, unionBy } from "lodash";
|
import { merge, sortBy, unionBy } from "lodash";
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||||
import { Group } from "@/interfaces/Group";
|
import { Group } from "@/interfaces/Group";
|
||||||
@@ -25,10 +24,9 @@ const GroupContext = React.createContext(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default function GroupsProvider({ children }: Props) {
|
export default function GroupsProvider({ children }: Props) {
|
||||||
const path = usePathname();
|
|
||||||
const { permission } = useLoggedInUser();
|
const { permission } = useLoggedInUser();
|
||||||
|
|
||||||
return path === "/peers" && permission.dashboard_view == "blocked" ? (
|
return permission.dashboard_view == "blocked" ? (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
) : (
|
) : (
|
||||||
<GroupsProviderContent>{children}</GroupsProviderContent>
|
<GroupsProviderContent>{children}</GroupsProviderContent>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const RoutesContext = React.createContext(
|
|||||||
toUpdate: Partial<Route>,
|
toUpdate: Partial<Route>,
|
||||||
onSuccess?: (route: Route) => void,
|
onSuccess?: (route: Route) => void,
|
||||||
message?: string,
|
message?: string,
|
||||||
|
options?: { remove_access_control_groups?: boolean },
|
||||||
) => void;
|
) => void;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -33,6 +34,7 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
|
|||||||
toUpdate: Partial<Route>,
|
toUpdate: Partial<Route>,
|
||||||
onSuccess?: (route: Route) => void,
|
onSuccess?: (route: Route) => void,
|
||||||
message?: string,
|
message?: string,
|
||||||
|
options?: { remove_access_control_groups?: boolean },
|
||||||
) => {
|
) => {
|
||||||
const hasDomains = route.domains ? route.domains.length > 0 : false;
|
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,
|
metric: toUpdate.metric ?? route.metric ?? 9999,
|
||||||
masquerade: toUpdate.masquerade ?? route.masquerade ?? true,
|
masquerade: toUpdate.masquerade ?? route.masquerade ?? true,
|
||||||
groups: toUpdate.groups ?? route.groups ?? [],
|
groups: toUpdate.groups ?? route.groups ?? [],
|
||||||
access_control_groups:
|
access_control_groups: options?.remove_access_control_groups
|
||||||
toUpdate.access_control_groups ??
|
? undefined
|
||||||
route.access_control_groups ??
|
: toUpdate.access_control_groups ??
|
||||||
undefined,
|
route.access_control_groups ??
|
||||||
|
undefined,
|
||||||
},
|
},
|
||||||
`/${route.id}`,
|
`/${route.id}`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||||
import useFetchApi from "@utils/api";
|
import useFetchApi from "@utils/api";
|
||||||
|
import { isNetBirdHosted } from "@utils/netbird";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Permission } from "@/interfaces/Permission";
|
import { Permission } from "@/interfaces/Permission";
|
||||||
import { User } from "@/interfaces/User";
|
import { Role, User } from "@/interfaces/User";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -40,8 +41,13 @@ export const useUsers = () => React.useContext(UsersContext);
|
|||||||
|
|
||||||
export const useLoggedInUser = () => {
|
export const useLoggedInUser = () => {
|
||||||
const { loggedInUser } = useUsers();
|
const { loggedInUser } = useUsers();
|
||||||
const isOwner = loggedInUser ? loggedInUser?.role === "owner" : false;
|
const isOwner = loggedInUser ? loggedInUser?.role === Role.Owner : false;
|
||||||
const isAdmin = loggedInUser ? loggedInUser?.role === "admin" : false;
|
const isAdmin = loggedInUser ? loggedInUser?.role === Role.Admin : false;
|
||||||
|
const isBillingAdmin = isNetBirdHosted()
|
||||||
|
? loggedInUser
|
||||||
|
? loggedInUser?.role === Role.BillingAdmin
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
const isUser = !isOwner && !isAdmin;
|
const isUser = !isOwner && !isAdmin;
|
||||||
const isOwnerOrAdmin = isOwner || isAdmin;
|
const isOwnerOrAdmin = isOwner || isAdmin;
|
||||||
|
|
||||||
@@ -56,6 +62,7 @@ export const useLoggedInUser = () => {
|
|||||||
isOwner,
|
isOwner,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isUser,
|
isUser,
|
||||||
|
isBillingAdmin,
|
||||||
isOwnerOrAdmin,
|
isOwnerOrAdmin,
|
||||||
permission,
|
permission,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -35,4 +35,5 @@ export interface GroupedRoute {
|
|||||||
description?: string;
|
description?: string;
|
||||||
description_search?: string;
|
description_search?: string;
|
||||||
domain_search?: string;
|
domain_search?: string;
|
||||||
|
routes_search?: string;
|
||||||
}
|
}
|
||||||
@@ -18,4 +18,5 @@ export enum Role {
|
|||||||
User = "user",
|
User = "user",
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Owner = "owner",
|
Owner = "owner",
|
||||||
|
BillingAdmin = "billing_admin",
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ export default function PageContainer({ children, className }: Props) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
className,
|
className,
|
||||||
"relative flex-auto overflow-auto bg-nb-gray z-1",
|
"relative flex-auto overflow-auto bg-nb-gray z-1",
|
||||||
|
"focus:outline-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ export default function AccessTokenActionCell({ access_token }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"flex justify-end pr-4"}>
|
<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} />
|
<Trash2 size={16} />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default function CreateAccessTokenModal({ children, user }: Props) {
|
|||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
className={"w-full"}
|
className={"w-full"}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
data-cy={"access-token-copy-close"}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
@@ -170,6 +171,7 @@ export function AccessTokenModalContent({ onSuccess, user }: ModalProps) {
|
|||||||
<Label>Name</Label>
|
<Label>Name</Label>
|
||||||
<HelpText>Set an easily identifiable name for your token</HelpText>
|
<HelpText>Set an easily identifiable name for your token</HelpText>
|
||||||
<Input
|
<Input
|
||||||
|
data-cy={"access-token-name"}
|
||||||
placeholder={"e.g., Infra token"}
|
placeholder={"e.g., Infra token"}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
@@ -184,6 +186,7 @@ export function AccessTokenModalContent({ onSuccess, user }: ModalProps) {
|
|||||||
<Input
|
<Input
|
||||||
maxWidthClass={"max-w-[200px]"}
|
maxWidthClass={"max-w-[200px]"}
|
||||||
placeholder={"30"}
|
placeholder={"30"}
|
||||||
|
data-cy={"access-token-expires-in"}
|
||||||
min={1}
|
min={1}
|
||||||
max={365}
|
max={365}
|
||||||
value={expiresIn}
|
value={expiresIn}
|
||||||
@@ -215,7 +218,12 @@ export function AccessTokenModalContent({ onSuccess, user }: ModalProps) {
|
|||||||
<Button variant={"secondary"}>Cancel</Button>
|
<Button variant={"secondary"}>Cancel</Button>
|
||||||
</ModalClose>
|
</ModalClose>
|
||||||
|
|
||||||
<Button variant={"primary"} onClick={submit} disabled={isDisabled}>
|
<Button
|
||||||
|
variant={"primary"}
|
||||||
|
onClick={submit}
|
||||||
|
disabled={isDisabled}
|
||||||
|
data-cy={"create-access-token"}
|
||||||
|
>
|
||||||
<PlusCircle size={16} />
|
<PlusCircle size={16} />
|
||||||
Create Token
|
Create Token
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import useFetchApi from "@utils/api";
|
import useFetchApi from "@utils/api";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||||
import { Account } from "@/interfaces/Account";
|
import { Account } from "@/interfaces/Account";
|
||||||
|
|
||||||
export const useAccount = () => {
|
export const useAccount = () => {
|
||||||
const { data: accounts } = useFetchApi<Account[]>("/accounts");
|
const { isBillingAdmin } = useLoggedInUser();
|
||||||
|
const { data: accounts } = useFetchApi<Account[]>(
|
||||||
|
"/accounts",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
!isBillingAdmin,
|
||||||
|
);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!accounts) return;
|
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
Reference in new issue
Block a user