Sync with cloud (#491)

implements a "Sync with cloud" functionality that includes various UI improvements, code refactoring, and component extractions. The changes focus on enhancing the user interface, improving code organization, and adding new features for remote access and activity tracking.

- Refactors inline components into reusable shared components
- Adds new activity tracking for group operations
- Updates remote access configuration and UI components
- Enhances styling and layout for better user experience
This commit is contained in:
Eduard Gert
2025-10-03 14:37:11 +02:00
committed by GitHub
parent bc4aac10aa
commit 831673d0d6
33 changed files with 729 additions and 593 deletions

View File

@@ -71,7 +71,6 @@ jobs:
images: ${{ env.IMAGE_NAME }}
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.NB_DOCKER_USER }}
@@ -82,7 +81,7 @@ jobs:
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
push: true
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -66,8 +66,8 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
export default function PeerPage() {
const queryParameter = useSearchParams();
@@ -350,15 +350,15 @@ const PeerGeneralInformation = () => {
/>
</FullTooltip>
{/* Remote Access Buttons */}
<div>
<Label>Remote Access</Label>
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
<div className="flex gap-3">
<SSHButton peer={peer} />
<RDPButton peer={peer} />
</div>
{/* Remote Access Buttons */}
<div>
<Label>Remote Access</Label>
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
<div className="flex gap-3">
<SSHButton peer={peer} />
<RDPButton peer={peer} />
</div>
</div>
{permission.groups.read && (
<div>
@@ -582,6 +582,23 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
/>
)}
{peer.created_at && (
<Card.ListItem
label={
<>
<CalendarDays size={16} />
Registered on
</>
}
value={
dayjs(peer.created_at).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(peer.created_at) +
")"
}
/>
)}
<Card.ListItem
label={
<>

View File

@@ -35,6 +35,7 @@ export default function NotFound() {
}
const Redirect = ({ url, queryParams }: Props) => {
useRedirect("/peers" + (queryParams && `?${queryParams}`));
const params = queryParams && `?${queryParams}`;
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`);
return <FullScreenLoading />;
};

View File

@@ -58,6 +58,8 @@ export default function OIDCProvider({ children }: Props) {
"utm_content",
"utm_campaign",
"hs_id",
"page",
"page_size",
"user",
"port",
];

View File

@@ -34,7 +34,7 @@ export const buttonVariants = cva(
secondary: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
secondaryLighter: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",

View File

@@ -25,6 +25,8 @@ type Props = {
customOnOpenChange?: React.Dispatch<React.SetStateAction<boolean>>;
delayDuration?: number;
skipDelayDuration?: number;
alignOffset?: number;
sideOffset?: number;
} & TooltipProps &
TooltipVariants;
@@ -45,6 +47,8 @@ export default function FullTooltip({
delayDuration = 1,
skipDelayDuration = 300,
variant = "default",
alignOffset = 20,
sideOffset,
}: Props) {
const [open, setOpen] = useState(!!keepOpen);
@@ -83,7 +87,8 @@ export default function FullTooltip({
)}
{!disabled && (
<TooltipContent
alignOffset={20}
alignOffset={alignOffset}
sideOffset={sideOffset}
forceMount={true}
className={contentClassName}
variant={variant}

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import { cn } from "@utils/helpers";
export const ListItem = ({
icon,
label,
value,
className,
}: {
icon?: React.ReactNode;
label: string;
value: string | React.ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
className,
)}
>
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
{icon}
{label}
</div>
<div className={"text-nb-gray-300"}>{value}</div>
</div>
);
};

View File

@@ -0,0 +1,44 @@
import InlineLink from "@components/InlineLink";
import SquareIcon from "@components/SquareIcon";
import AddPeerButton from "@components/ui/AddPeerButton";
import GetStartedTest from "@components/ui/GetStartedTest";
import { ExternalLinkIcon } from "lucide-react";
import * as React from "react";
import PeerIcon from "@/assets/icons/PeerIcon";
type Props = {
showBackground?: boolean;
};
export const NoPeersGettingStarted = ({ showBackground = true }) => {
return (
<GetStartedTest
showBackground={showBackground}
icon={
<SquareIcon
icon={<PeerIcon className={"fill-nb-gray-200"} size={20} />}
color={"gray"}
size={"large"}
/>
}
title={"Get Started with NetBird"}
description={
"It looks like you don't have any connected machines.\n" +
"Get started by adding one to your network."
}
button={<AddPeerButton />}
learnMore={
<>
Learn more in our{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/getting-started"}
target={"_blank"}
>
Getting Started Guide
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
);
};

View File

@@ -42,13 +42,13 @@ import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
import { PolicyRuleResource } from "@/interfaces/Policy";
import { User } from "@/interfaces/User";
import { HorizontalUsersStack } from "@/modules/users/HorizontalUsersStack";
import { PeerOperatingSystemIcon } from "@/modules/peers/PeerOperatingSystemIcon";
import { HorizontalUsersStack } from "@/modules/users/HorizontalUsersStack";
const groupsSearchPredicate = (item: Group, query: string) => {
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item?.id?.toLowerCase().includes(lowerCaseQuery) ?? false;
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item?.id?.toLowerCase().includes(lowerCaseQuery) ?? false;
};
interface MultiSelectProps {
@@ -104,11 +104,11 @@ export function PeerGroupSelector({
placeholderForSearch = 'Search groups or add new group by pressing "Enter"...',
}: Readonly<MultiSelectProps>) {
const { data: resources, isLoading: isResourcesLoading } = useFetchApi<
NetworkResource[]
NetworkResource[]
>("/networks/resources");
const { data: peers, isLoading: isPeersLoading } =
useFetchApi<Peer[]>("/peers");
useFetchApi<Peer[]>("/peers");
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
useGroups();
@@ -118,18 +118,19 @@ export function PeerGroupSelector({
const [inputRef, { width }] = useElementSize<
HTMLButtonElement | HTMLSpanElement
>();
const [open, setOpen] = useState(false);
const sortedDropdownOptions = useSortedDropdownOptions(
dropdownOptions,
values,
open,
dropdownOptions,
values,
open,
);
const [filteredGroups, search, setSearch] = useSearch(
sortedDropdownOptions,
groupsSearchPredicate,
{ filter: true, debounce: 150 },
sortedDropdownOptions,
groupsSearchPredicate,
{ filter: true, debounce: 150 },
);
// Update dropdown options when groups change
@@ -247,10 +248,10 @@ export function PeerGroupSelector({
}, [tab]);
const searchPlaceholder = useMemo(() => {
if (tab === "groups") return placeholderForSearch;
if (tab === "resources") return "Search resource...";
if (tab === "peers") return "Search peer...";
return "Search...";
if (tab === "groups") return placeholderForSearch;
if (tab === "resources") return "Search resource...";
if (tab === "peers") return "Search peer...";
return "Search...";
}, [tab, placeholderForSearch]);
const selectResource = (resource?: NetworkResource) => {
@@ -266,12 +267,12 @@ export function PeerGroupSelector({
};
const selectPeer = (peer?: Peer) => {
if (!peer?.id) return;
onResourceChange?.({
id: peer.id,
type: "peer",
});
onChange([]);
if (!peer?.id) return;
onResourceChange?.({
id: peer.id,
type: "peer",
});
onChange([]);
};
return (
@@ -389,7 +390,7 @@ export function PeerGroupSelector({
side={side}
sideOffset={10}
>
<Command className={"w-full flex"} loop shouldFilter={false}>
<Command className={"w-full flex"} loop shouldFilter={false}>
<CommandList className={"w-full"}>
<div className={"relative"}>
<CommandInput
@@ -430,17 +431,17 @@ export function PeerGroupSelector({
</div>
<Tabs defaultValue={"groups"} value={tab} onValueChange={setTab}>
<TabTriggers
searchRef={searchRef}
showPeers={showPeers}
showResources={showResources}
/>
<TabTriggers
searchRef={searchRef}
showPeers={showPeers}
showResources={showResources}
/>
<TabsContent value={"groups"} className={"p-0 my-0"}>
<CommandGroup>
<ScrollArea
className={cn(
"max-h-[195px] flex flex-col gap-1 pl-2 py-2 pr-3",
filteredGroups.length == 0 && !search && "py-0",
filteredGroups.length == 0 && !search && "py-0",
)}
>
{searchedGroupNotFound && (
@@ -453,8 +454,8 @@ export function PeerGroupSelector({
value={search}
onClick={(e) => e.preventDefault()}
>
<Badge variant={"gray-ghost"} className={"h-7"}>
<FolderGit2 size={12} className={"shrink-0"} />
<Badge variant={"gray-ghost"} className={"h-7"}>
<FolderGit2 size={12} className={"shrink-0"} />
{search}
</Badge>
<div
@@ -510,11 +511,11 @@ export function PeerGroupSelector({
onClick={(e) => e.preventDefault()}
>
<div className={"flex items-center gap-2"}>
<GroupBadge
group={option}
showNewBadge={true}
className={"h-7"}
/>
<GroupBadge
group={option}
showNewBadge={true}
className={"h-7"}
/>
</div>
<div className={"flex items-center gap-5"}>
@@ -533,10 +534,10 @@ export function PeerGroupSelector({
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
}
>
<MonitorSmartphoneIcon
size={14}
className={"shrink-0"}
/>
<MonitorSmartphoneIcon
size={14}
className={"shrink-0"}
/>
{peerCount} Peer(s)
</div>
) : (
@@ -568,17 +569,17 @@ export function PeerGroupSelector({
/>
</TabsContent>
)}
{showPeers && (
<TabsContent value={"peers"} className={"p-0 my-0"}>
<PeersList
search={search}
peers={peers}
isLoading={isPeersLoading}
value={resource}
onChange={selectPeer}
/>
</TabsContent>
)}
{showPeers && (
<TabsContent value={"peers"} className={"p-0 my-0"}>
<PeersList
search={search}
peers={peers}
isLoading={isPeersLoading}
value={resource}
onChange={selectPeer}
/>
</TabsContent>
)}
</Tabs>
</CommandList>
</Command>
@@ -597,6 +598,7 @@ const TabTriggers = ({
showPeers?: boolean;
}) => {
if (!showResources && !showPeers) return null;
return (
<TabsList justify={"start"} className={"px-3"}>
<TabsTrigger
@@ -613,37 +615,37 @@ const TabTriggers = ({
Groups
</TabsTrigger>
{showResources && (
<TabsTrigger
value={"resources"}
className={"text-[.8rem] font-normal"}
onClick={() => searchRef.current?.focus()}
>
<Layers3Icon
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
size={14}
/>
Resources
</TabsTrigger>
)}
{showResources && (
<TabsTrigger
value={"resources"}
className={"text-[.8rem] font-normal"}
onClick={() => searchRef.current?.focus()}
>
<Layers3Icon
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
size={14}
/>
Resources
</TabsTrigger>
)}
{showPeers && (
<TabsTrigger
value={"peers"}
className={"text-[.8rem] font-normal"}
onClick={() => searchRef.current?.focus()}
>
<MonitorSmartphoneIcon
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
size={14}
/>
Peers
</TabsTrigger>
)}
{showPeers && (
<TabsTrigger
value={"peers"}
className={"text-[.8rem] font-normal"}
onClick={() => searchRef.current?.focus()}
>
<MonitorSmartphoneIcon
className={
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
}
size={14}
/>
Peers
</TabsTrigger>
)}
</TabsList>
);
};
@@ -800,105 +802,105 @@ const ResourcesList = ({
};
const peersSearchPredicate = (item: Peer, query: string) => {
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item.ip.toLowerCase().includes(lowerCaseQuery);
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item.ip.toLowerCase().includes(lowerCaseQuery);
};
const PeersList = ({
search,
peers,
isLoading,
value,
onChange,
}: {
search: string;
peers?: Peer[];
isLoading: boolean;
value?: PolicyRuleResource;
onChange: (peer: Peer) => void;
search,
peers,
isLoading,
value,
onChange,
}: {
search: string;
peers?: Peer[];
isLoading: boolean;
value?: PolicyRuleResource;
onChange: (peer: Peer) => void;
}) => {
const [filteredItems, _, setSearch] = useSearch(
peers || [],
peersSearchPredicate,
{ filter: true, debounce: 150 },
);
const [filteredItems, _, setSearch] = useSearch(
peers || [],
peersSearchPredicate,
{ filter: true, debounce: 150 },
);
useEffect(() => {
setSearch(search);
}, [search, setSearch]);
if (isLoading) {
return (
<div className={"max-h-[195px] flex flex-col gap-1 py-2 px-2"}>
<Skeleton height={42} className={"rounded-md"} />
<Skeleton height={42} className={"rounded-md"} />
<Skeleton height={42} className={"rounded-md"} />
<Skeleton height={42} className={"rounded-md"} />
</div>
);
}
if (search != "" && filteredItems.length == 0) {
return (
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
There are no peers matching your search. Please try a different search
term.
</DropdownInfoText>
);
}
if (search == "" && filteredItems.length == 0) {
return (
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
There are no peers available yet. <br />
Go to <InlineLink href={"/peers"}>Peers</InlineLink> to add some peers.
</DropdownInfoText>
);
}
useEffect(() => {
setSearch(search);
}, [search, setSearch]);
if (isLoading) {
return (
<Radio defaultValue={value?.id} name={"peer"} value={value?.id}>
<VirtualScrollAreaList
items={filteredItems}
onSelect={onChange}
itemClassName={"dark:aria-selected:bg-nb-gray-800/20"}
renderItem={(res) => {
if (!res?.id) return;
return (
<Fragment key={res.id}>
<div className={"flex items-center gap-2"}>
<Badge
useHover={true}
data-cy={"group-badge"}
variant={"gray-ghost"}
className={cn(
"transition-all group whitespace-nowrap h-7 px-2",
)}
onClick={(e) => {
e.preventDefault();
}}
>
<PeerOperatingSystemIcon os={res.os} />
<TextWithTooltip text={res?.name || ""} maxChars={20} />
</Badge>
</div>
<div className={"flex items-center gap-5"}>
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
}
>
{res.ip}
<RadioItem value={res.id} />
</div>
</div>
</Fragment>
);
}}
/>
</Radio>
<div className={"max-h-[195px] flex flex-col gap-1 py-2 px-2"}>
<Skeleton height={42} className={"rounded-md"} />
<Skeleton height={42} className={"rounded-md"} />
<Skeleton height={42} className={"rounded-md"} />
<Skeleton height={42} className={"rounded-md"} />
</div>
);
};
}
if (search != "" && filteredItems.length == 0) {
return (
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
There are no peers matching your search. Please try a different search
term.
</DropdownInfoText>
);
}
if (search == "" && filteredItems.length == 0) {
return (
<DropdownInfoText className={"mt-5 max-w-sm mx-auto"}>
There are no peers available yet. <br />
Go to <InlineLink href={"/peers"}>Peers</InlineLink> to add some peers.
</DropdownInfoText>
);
}
return (
<Radio defaultValue={value?.id} name={"peer"} value={value?.id}>
<VirtualScrollAreaList
items={filteredItems}
onSelect={onChange}
itemClassName={"dark:aria-selected:bg-nb-gray-800/20"}
renderItem={(res) => {
if (!res?.id) return;
return (
<Fragment key={res.id}>
<div className={"flex items-center gap-2"}>
<Badge
useHover={true}
data-cy={"group-badge"}
variant={"gray-ghost"}
className={cn(
"transition-all group whitespace-nowrap h-7 px-2",
)}
onClick={(e) => {
e.preventDefault();
}}
>
<PeerOperatingSystemIcon os={res.os} />
<TextWithTooltip text={res?.name || ""} maxChars={20} />
</Badge>
</div>
<div className={"flex items-center gap-5"}>
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
}
>
{res.ip}
<RadioItem value={res.id} />
</div>
</div>
</Fragment>
);
}}
/>
</Radio>
);
};

View File

@@ -44,10 +44,12 @@ function Trigger({
children,
value,
disabled = false,
className,
}: {
children: React.ReactNode;
value: string;
disabled?: boolean;
className?: string;
}) {
const currentValue = useTabContext();
return (
@@ -60,6 +62,7 @@ function Trigger({
: disabled
? ""
: "text-nb-gray-400 hover:bg-nb-gray-900/50",
className,
)}
value={value}
>

View File

@@ -37,6 +37,10 @@ interface SelectDropdownProps {
searchPlaceholder?: string;
isLoading?: boolean;
variant?: ButtonVariants["variant"];
className?: string;
size?: "xs" | "sm";
children?: React.ReactNode;
maxHeight?: number;
}
export function SelectDropdown({
@@ -51,6 +55,10 @@ export function SelectDropdown({
searchPlaceholder = "Search...",
isLoading = false,
variant = "input",
className,
size = "sm",
children,
maxHeight,
}: Readonly<SelectDropdownProps>) {
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
@@ -79,6 +87,46 @@ export function SelectDropdown({
});
}, [options, debouncedSearch]);
const Loading = () => {
return (
<div className={"flex items-center gap-2"}>
<Skeleton width={20} />
<Skeleton width={100} />
</div>
);
};
const SelectedItem = () => {
return (
<div className={"flex items-center gap-2.5"}>
{selected?.icon && <selected.icon size={14} width={14} />}
<div
className={cn(
"flex flex-col text-sm font-medium",
size === "xs" && "text-xs",
)}
>
<span className={"text-nb-gray-200"}>{selected?.label}</span>
</div>
</div>
);
};
const PlaceholderItem = () => {
return (
<div className={"flex items-center gap-2.5"}>
<div
className={cn(
"flex flex-col text-sm font-medium",
size === "xs" && "text-xs",
)}
>
<span className={"text-nb-gray-200"}>{placeholder}</span>
</div>
</div>
);
};
return (
<Popover
open={open}
@@ -91,45 +139,26 @@ export function SelectDropdown({
setOpen(isOpen);
}}
>
<PopoverTrigger asChild={true} disabled={disabled || isLoading}>
<Button
variant={variant}
disabled={disabled || isLoading}
ref={inputRef}
className={"w-full"}
>
<div className={"w-full flex justify-between items-center gap-2"}>
{isLoading ? (
<div className={"flex gap-2"}>
<Skeleton width={20} />
<Skeleton width={100} />
<PopoverTrigger asChild={!children} disabled={disabled || isLoading}>
{children ? (
children
) : (
<Button
variant={variant}
disabled={disabled || isLoading}
ref={inputRef}
className={cn("w-full", className)}
>
<div className={"w-full flex justify-between items-center gap-2"}>
{isLoading && <Loading />}
{!isLoading && selected && <SelectedItem />}
{!isLoading && !selected && <PlaceholderItem />}
<div className={"pl-2"}>
<ChevronsUpDown size={18} className={"shrink-0"} />
</div>
) : selected ? (
<React.Fragment>
<div className={"flex items-center gap-2.5"}>
{selected?.icon && <selected.icon size={14} width={14} />}
<div className={"flex flex-col text-sm font-medium"}>
<span className={"text-nb-gray-200"}>
{selected?.label}
</span>
</div>
</div>
</React.Fragment>
) : (
<React.Fragment>
<div className={"flex items-center gap-2.5"}>
<div className={"flex flex-col text-sm font-medium"}>
<span className={"text-nb-gray-200"}>{placeholder}</span>
</div>
</div>
</React.Fragment>
)}
<div className={"pl-2"}>
<ChevronsUpDown size={18} className={"shrink-0"} />
</div>
</div>
</Button>
</Button>
)}
</PopoverTrigger>
<PopoverContent
className="w-full p-0 shadow-sm shadow-nb-gray-950 focus:outline-none"
@@ -164,18 +193,22 @@ export function SelectDropdown({
<ScrollArea
className={cn(
"max-h-[380px] overflow-y-auto flex flex-col gap-1 pl-2 pb-2 pr-3",
"overflow-y-auto flex flex-col gap-1 pl-2 pr-3",
!showSearch && "pt-2",
)}
style={{
maxHeight: maxHeight ?? 380,
}}
>
<CommandGroup>
<div className={"grid grid-cols-1 gap-1"}>
<div className={"grid grid-cols-1 gap-1 pb-2"}>
{filteredItems.map((option) => (
<SelectDropdownItem
option={option}
toggle={toggle}
key={option.value}
showValue={showValues}
size={size}
/>
))}
</div>
@@ -192,10 +225,12 @@ const SelectDropdownItem = ({
option,
toggle,
showValue = false,
size = "sm",
}: {
option: SelectOption;
toggle: (value: string) => void;
showValue?: boolean;
size: "xs" | "sm";
}) => {
const value = option.value || "" + option.label || "";
const elementRef = useRef<HTMLDivElement>(null);
@@ -221,13 +256,20 @@ const SelectDropdownItem = ({
>
<div className={"flex items-center gap-2.5 p-1"}>
{option.icon && <option.icon size={14} width={14} />}
<div className={"flex flex-col text-sm font-medium"}>
<div
className={cn(
"flex flex-col text-sm font-medium",
size === "xs" && "text-xs",
)}
>
<span className={"text-nb-gray-200"}>{option.label}</span>
</div>
</div>
{showValue && (
<div className={"flex items-center gap-2.5 p-1"}>
<Paragraph className={cn("text-sm text-right")}>
<Paragraph
className={cn("text-sm text-right", size === "xs" && "text-xs")}
>
{option.value}
</Paragraph>
</div>

View File

@@ -1,4 +1,3 @@
import { IconArrowBack } from "@tabler/icons-react";
import { cn } from "@utils/helpers";
import { SearchIcon } from "lucide-react";
import * as React from "react";
@@ -38,15 +37,9 @@ export const SelectDropdownSearchInput = forwardRef<HTMLInputElement, Props>(
<SearchIcon size={14} />
</div>
</div>
<div className={"absolute right-0 top-0 h-full flex items-center pr-4"}>
<div
className={
"flex items-center bg-nb-gray-800 py-1 px-1.5 rounded-[4px] border border-nb-gray-500"
}
>
<IconArrowBack size={10} />
</div>
</div>
<div
className={"absolute right-0 top-0 h-full flex items-center pr-4"}
></div>
</div>
);
},

View File

@@ -10,6 +10,7 @@ type Props = {
description?: string;
button?: React.ReactNode;
learnMore?: React.ReactNode;
showBackground?: boolean;
};
export default function GetStartedTest({
@@ -18,28 +19,33 @@ export default function GetStartedTest({
description,
button,
learnMore,
showBackground = true,
}: Props) {
return (
<div className={"px-8 mt-8"}>
<Card className={"w-full relative overflow-hidden"}>
<div
className={
"absolute z-20 bg-gradient-to-b dark:to-nb-gray-950 dark:from-nb-gray-950/40 w-full h-full"
}
></div>
<div
className={
"absolute w-full h-full left-0 top-0 z-10 px-5 py-3 overflow-hidden"
}
>
<div className={"flex flex-col gap-2"}>
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
</div>
</div>
{showBackground && (
<>
<div
className={
"absolute z-20 bg-gradient-to-b dark:to-nb-gray-950 dark:from-nb-gray-950/40 w-full h-full"
}
></div>
<div
className={
"absolute w-full h-full left-0 top-0 z-10 px-5 py-3 overflow-hidden"
}
>
<div className={"flex flex-col gap-2"}>
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
<Skeleton className={"w-full"} height={70} duration={4} />
</div>
</div>
</>
)}
<div className={"w-full h-full z-20 relative left-0 top-0 flex py-8"}>
<div className={"inline-flex justify-center w-full"}>
<div>

View File

@@ -11,9 +11,11 @@ import { useGroupIdentification } from "@/modules/groups/useGroupIdentification"
export const GroupBadgeIcon = ({
id,
issued,
size = 12,
}: {
id?: string;
issued?: GroupIssued;
size?: number;
}) => {
const { groups } = useGroups();
const group = groups?.find((g) => g.id === id);
@@ -22,11 +24,12 @@ export const GroupBadgeIcon = ({
useGroupIdentification({ id, issued: issued ?? group?.issued });
if (isGoogleGroup)
return <GoogleIcon size={11} className={"shrink-0 mr-0.5"} />;
return <GoogleIcon size={size - 1} className={"shrink-0 mr-0.5"} />;
if (isAzureGroup)
return <EntraIcon size={13} className={"shrink-0 mr-0.5"} />;
if (isOktaGroup) return <OktaIcon size={11} className={"shrink-0 mr-0.5"} />;
if (isJWTGroup) return <JWTIcon size={12} className={"shrink-0"} />;
return <EntraIcon size={size + 1} className={"shrink-0 mr-0.5"} />;
if (isOktaGroup)
return <OktaIcon size={size - 1} className={"shrink-0 mr-0.5"} />;
if (isJWTGroup) return <JWTIcon size={size} className={"shrink-0"} />;
return <FolderGit2 size={12} className={"shrink-0"} />;
return <FolderGit2 size={size} className={"shrink-0"} />;
};

View File

@@ -62,7 +62,6 @@ const UserProfileProvider = ({ children }: Props) => {
}
}, [user, error, users, isLoading, isAllUsersLoading]);
const data = useMemo(() => {
return {
loggedInUser,

View File

@@ -23,6 +23,7 @@ export default function useOperatingSystem() {
* Falls back to Linux if the operating system is not recognized
*/
export const getOperatingSystem = (os: string) => {
if (!os) return OperatingSystem.LINUX as const;
if (os.toLowerCase().includes("freebsd"))
return OperatingSystem.FREEBSD as const;
if (os.toLowerCase().includes("darwin"))

View File

@@ -6,6 +6,7 @@ export interface Peer {
name: string;
ip: string;
connected: boolean;
created_at?: Date;
last_seen: Date;
os: string;
version: string;

View File

@@ -1,5 +1,6 @@
import Button from "@components/Button";
import ButtonGroup from "@components/ButtonGroup";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import SquareIcon from "@components/SquareIcon";
import { DataTable } from "@components/table/DataTable";
@@ -29,7 +30,6 @@ import AccessControlPortsCell from "@/modules/access-control/table/AccessControl
import AccessControlPostureCheckCell from "@/modules/access-control/table/AccessControlPostureCheckCell";
import AccessControlProtocolCell from "@/modules/access-control/table/AccessControlProtocolCell";
import AccessControlSourcesCell from "@/modules/access-control/table/AccessControlSourcesCell";
import FullTooltip from "@components/FullTooltip";
type Props = {
policies?: Policy[];
@@ -201,40 +201,40 @@ export default function AccessControlTable({
const [currentRow, setCurrentRow] = useState<Policy>();
const [currentCellClicked, setCurrentCellClicked] = useState("");
const [showTemporaryPolicies, setShowTemporaryPolicies] = useState(false);
const [showTemporaryPolicies, setShowTemporaryPolicies] = useState(false);
const withTemporaryPolicies = useCallback(
(condition: boolean) =>
policies?.filter((policy) =>
condition
? policy?.name?.startsWith("Temporary") &&
policy?.name?.endsWith("client") &&
policy?.description?.startsWith("Temporary") &&
policy?.description?.endsWith("client")
: !(
policy?.name?.startsWith("Temporary") &&
policy?.name?.endsWith("client") &&
policy?.description?.startsWith("Temporary") &&
policy?.description?.endsWith("client")
),
) ?? [],
[policies],
);
const withTemporaryPolicies = useCallback(
(condition: boolean) =>
policies?.filter((policy) =>
condition
? policy?.name?.startsWith("Temporary") &&
policy?.name?.endsWith("client") &&
policy?.description?.startsWith("Temporary") &&
policy?.description?.endsWith("client")
: !(
policy?.name?.startsWith("Temporary") &&
policy?.name?.endsWith("client") &&
policy?.description?.startsWith("Temporary") &&
policy?.description?.endsWith("client")
),
) ?? [],
[policies],
);
const tempPolicies = useMemo(
() => withTemporaryPolicies(true),
[withTemporaryPolicies],
);
const regularPolicies = useMemo(
() => withTemporaryPolicies(false),
[withTemporaryPolicies],
);
const tempPolicies = useMemo(
() => withTemporaryPolicies(true),
[withTemporaryPolicies],
);
const regularPolicies = useMemo(
() => withTemporaryPolicies(false),
[withTemporaryPolicies],
);
useEffect(() => {
if (showTemporaryPolicies && tempPolicies?.length === 0) {
setShowTemporaryPolicies(false);
}
}, [showTemporaryPolicies, tempPolicies]);
useEffect(() => {
if (showTemporaryPolicies && tempPolicies?.length === 0) {
setShowTemporaryPolicies(false);
}
}, [showTemporaryPolicies, tempPolicies]);
return (
<>
@@ -338,91 +338,91 @@ export default function AccessControlTable({
</>
)}
>
{(table) => {
return (
<>
<ButtonGroup disabled={policies?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("enabled")?.setFilterValue(undefined);
}}
disabled={policies?.length == 0}
variant={
table.getColumn("enabled")?.getFilterValue() === undefined
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("enabled")?.setFilterValue(true);
}}
disabled={policies?.length == 0}
variant={
table.getColumn("enabled")?.getFilterValue() === true
? "tertiary"
: "secondary"
}
>
Active
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("enabled")?.setFilterValue(false);
}}
disabled={policies?.length == 0}
variant={
table.getColumn("enabled")?.getFilterValue() === false
? "tertiary"
: "secondary"
}
>
Inactive
</ButtonGroup.Button>
</ButtonGroup>
<DataTableRowsPerPage
table={table}
disabled={policies?.length == 0}
/>
{(table) => {
return (
<>
<ButtonGroup disabled={policies?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("enabled")?.setFilterValue(undefined);
}}
disabled={policies?.length == 0}
variant={
table.getColumn("enabled")?.getFilterValue() === undefined
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("enabled")?.setFilterValue(true);
}}
disabled={policies?.length == 0}
variant={
table.getColumn("enabled")?.getFilterValue() === true
? "tertiary"
: "secondary"
}
>
Active
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("enabled")?.setFilterValue(false);
}}
disabled={policies?.length == 0}
variant={
table.getColumn("enabled")?.getFilterValue() === false
? "tertiary"
: "secondary"
}
>
Inactive
</ButtonGroup.Button>
</ButtonGroup>
<DataTableRowsPerPage
table={table}
disabled={policies?.length == 0}
/>
{tempPolicies?.length > 0 && (
<FullTooltip
content={
<div className={"max-w-sm text-xs"}>
Show temporary policies created by the NetBird browser
client. These policies are ephemeral and will be
deleted automatically after a short period of time.
</div>
}
>
<Button
className={"h-[44px]"}
variant={showTemporaryPolicies ? "tertiary" : "secondary"}
onClick={() => {
setShowTemporaryPolicies(!showTemporaryPolicies);
}}
>
<ClockFadingIcon size={16} />
</Button>
</FullTooltip>
)}
{tempPolicies?.length > 0 && (
<FullTooltip
content={
<div className={"max-w-sm text-xs"}>
Show temporary policies created by the NetBird browser
client. These policies are ephemeral and will be deleted
automatically after a short period of time.
</div>
}
>
<Button
className={"h-[44px]"}
variant={showTemporaryPolicies ? "tertiary" : "secondary"}
onClick={() => {
setShowTemporaryPolicies(!showTemporaryPolicies);
}}
>
<ClockFadingIcon size={16} />
</Button>
</FullTooltip>
)}
<DataTableRefreshButton
isDisabled={policies?.length == 0}
onClick={() => {
mutate("/policies").then();
mutate("/groups").then();
}}
/>
</>
);
}}
<DataTableRefreshButton
isDisabled={policies?.length == 0}
onClick={() => {
mutate("/policies").then();
mutate("/groups").then();
}}
/>
</>
);
}}
</DataTable>
</>
);
}
}

View File

@@ -260,9 +260,9 @@ export const useAccessControl = ({
updatePolicy(
policy,
policyObj,
() => {
(p) => {
mutate("/policies");
onSuccess && onSuccess(policy);
onSuccess && onSuccess(p);
},
"The policy was successfully saved",
);

View File

@@ -677,6 +677,20 @@ export default function ActivityDescription({ event }: Props) {
</div>
);
if (event.activity_code == "account.settings.extra.flow.group.remove")
return (
<div className={"inline"}>
Limit traffic event group <Value>{m.group_name}</Value> removed
</div>
);
if (event.activity_code == "account.settings.extra.flow.group.add")
return (
<div className={"inline"}>
Limit traffic event group <Value>{m.group_name}</Value> added
</div>
);
return (
<div className={"flex gap-2.5 items-center"}>
<span className={"mb-[1px]"}>{event.activity}</span>

View File

@@ -18,6 +18,7 @@ const ACTION_COLOR_MAPPING: Record<string, ActionStatus> = {
// Error actions
delete: ActionStatus.ERROR,
revoke: ActionStatus.ERROR,
remove: ActionStatus.ERROR,
block: ActionStatus.ERROR,
reject: ActionStatus.ERROR,

View File

@@ -20,9 +20,9 @@ export const NetworkInformationSquare = ({
return (
<button
className={cn(
"flex w-full items-center max-w-[300px] gap-4 dark:text-neutral-300 text-neutral-500 transition-all group/network rounded-md",
"flex w-full items-center max-w-[450px] gap-4 dark:text-neutral-300 text-neutral-500 transition-all group/network rounded-md",
onClick
? "hover:text-neutral-100 hover:bg-nb-gray-910 cursor-pointer py-2 pl-3 pr-5 relative"
? "hover:text-neutral-100 hover:bg-nb-gray-910 cursor-pointer py-2 pl-3 pr-14 relative"
: "cursor-default",
)}
onClick={onClick}
@@ -53,7 +53,7 @@ export const NetworkInformationSquare = ({
<div className={"mt-[0px] flex items-center flex-wrap"}>
<p
className={cn(
"font-medium",
"font-medium text-left whitespace-nowrap",
size == "md" ? "text-sm" : "text-xl leading-none mb-0.5",
)}
>

View File

@@ -1,5 +1,5 @@
import CopyToClipboardText from "@components/CopyToClipboardText";
import { cn } from "@utils/helpers";
import { ListItem } from "@components/ListItem";
import { FlagIcon, GlobeIcon, MapPin, NetworkIcon } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
@@ -104,30 +104,3 @@ export const PeerAddressTooltipContent = ({ peer }: Props) => {
</div>
);
};
const ListItem = ({
icon,
label,
value,
className,
}: {
icon: React.ReactNode;
label: string;
value: string | React.ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
className,
)}
>
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
{icon}
{label}
</div>
<div className={"text-nb-gray-300"}>{value}</div>
</div>
);
};

View File

@@ -22,35 +22,44 @@ export const PeerConnectButton = () => {
if (isMobile) return;
return isConnected ? (
<>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
asChild={true}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<div className={"group"}>
<ConnectButton />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-auto"
align="start"
side={"bottom"}
sideOffset={8}
>
<SSHButton peer={peer} isDropdown={true} />
<RDPButton peer={peer} isDropdown={true} />
</DropdownMenuContent>
</DropdownMenu>
</>
// <>
// <DropdownMenu modal={false}>
// <DropdownMenuTrigger
// asChild={true}
// onClick={(e) => {
// e.stopPropagation();
// e.preventDefault();
// }}
// >
// <div className={"group"}>
// <ConnectButton />
// </div>
// </DropdownMenuTrigger>
// <DropdownMenuContent
// className="w-auto"
// align="start"
// side={"bottom"}
// sideOffset={8}
// >
// <SSHButton peer={peer} isDropdown={true} />
// <RDPButton peer={peer} isDropdown={true} />
// </DropdownMenuContent>
// </DropdownMenu>
// </>
<FullTooltip
content={
<div className={"max-w-[200px] text-xs"}>
Connecting via SSH or RDP is coming soon.
</div>
}
>
<ConnectButton disabled={true} />
</FullTooltip>
) : (
<FullTooltip
content={
<div className={"max-w-[200px] text-xs"}>
Connecting via SSH or RDP is only available when the peer is online.
Connecting via SSH or RDP is coming soon.
</div>
}
>

View File

@@ -42,14 +42,14 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) {
active={peer.connected}
text={peer.name}
additionalInfo={
isOwnerOrAdmin && (
<>
<ExitNodePeerIndicator peer={peer} />
<EphemeralPeerIndicator peer={peer} />
<ExpirationDisabledIndicator peer={peer} />
<LoginRequiredIndicator peer={peer} />
</>
)
isOwnerOrAdmin && (
<>
<ExitNodePeerIndicator peer={peer} />
<EphemeralPeerIndicator peer={peer} />
<ExpirationDisabledIndicator peer={peer} />
<LoginRequiredIndicator peer={peer} />
</>
)
}
>
<div className={"text-nb-gray-400 font-light truncate"}>

View File

@@ -1,3 +1,4 @@
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import {
Tooltip,
@@ -8,13 +9,13 @@ import {
import MemoizedNetBirdIcon from "@components/ui/MemoizedNetBirdIcon";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { parseVersionString } from "@utils/version";
import { trim } from "lodash";
import { ArrowRightIcon, ArrowUpCircleIcon } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { PeerOperatingSystemIcon } from "@/modules/peers/PeerOperatingSystemIcon";
import FullTooltip from "@components/FullTooltip";
type Props = {
version: string;
@@ -38,6 +39,8 @@ export default function PeerVersionCell({ version, os, serial }: Props) {
return <ArrowUpCircleIcon size={15} className={"text-netbird"} />;
}, []);
const isWasmClient = trim(os) === "js";
return (
<div className={"flex flex-col gap-1"}>
{updateAvailable ? (
@@ -111,7 +114,7 @@ export default function PeerVersionCell({ version, os, serial }: Props) {
>
<PeerOperatingSystemIcon os={os} />
{os}
{isWasmClient ? "Web Client" : os}
</div>
</FullTooltip>
)}

View File

@@ -1,26 +1,24 @@
import Button from "@components/Button";
import ButtonGroup from "@components/ButtonGroup";
import { Checkbox } from "@components/Checkbox";
import InlineLink from "@components/InlineLink";
import SquareIcon from "@components/SquareIcon";
import FullTooltip from "@components/FullTooltip";
import { NoPeersGettingStarted } from "@components/NoPeersGettingStarted";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import AddPeerButton from "@components/ui/AddPeerButton";
import GetStartedTest from "@components/ui/GetStartedTest";
import { NotificationCountBadge } from "@components/ui/NotificationCountBadge";
import {
ColumnDef,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";
import { uniqBy } from "lodash";
import { ExternalLinkIcon } from "lucide-react";
import { trim, uniqBy } from "lodash";
import { MonitorDotIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import PeerIcon from "@/assets/icons/PeerIcon";
import PeerProvider from "@/contexts/PeerProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { useLoggedInUser } from "@/contexts/UsersProvider";
@@ -30,6 +28,7 @@ import { Peer } from "@/interfaces/Peer";
import { GroupFilterSelector } from "@/modules/groups/GroupFilterSelector";
import PeerActionCell from "@/modules/peers/PeerActionCell";
import PeerAddressCell from "@/modules/peers/PeerAddressCell";
import { PeerConnectButton } from "@/modules/peers/PeerConnectButton";
import PeerGroupCell from "@/modules/peers/PeerGroupCell";
import PeerLastSeenCell from "@/modules/peers/PeerLastSeenCell";
import { PeerMultiSelect } from "@/modules/peers/PeerMultiSelect";
@@ -37,9 +36,6 @@ import PeerNameCell from "@/modules/peers/PeerNameCell";
import { PeerOSCell } from "@/modules/peers/PeerOSCell";
import PeerStatusCell from "@/modules/peers/PeerStatusCell";
import PeerVersionCell from "@/modules/peers/PeerVersionCell";
import FullTooltip from "@components/FullTooltip";
import { MonitorDotIcon } from "lucide-react";
import { PeerConnectButton } from "@/modules/peers/PeerConnectButton";
const PeersTableColumns: ColumnDef<Peer>[] = [
{
@@ -76,14 +72,14 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
cell: ({ row }) => <PeerNameCell peer={row.original} />,
},
{
id: "connect",
accessorKey: "id",
header: "",
cell: ({ row }) => (
<PeerProvider peer={row.original}>
<PeerConnectButton />
</PeerProvider>
),
id: "connect",
accessorKey: "id",
header: "",
cell: ({ row }) => (
<PeerProvider peer={row.original}>
<PeerConnectButton />
</PeerProvider>
),
},
{
id: "approval_required",
@@ -170,11 +166,11 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
return <DataTableHeader column={column}>Version</DataTableHeader>;
},
cell: ({ row }) => (
<PeerVersionCell
version={row.original.version}
os={row.original.os}
serial={row.original.serial_number}
/>
<PeerVersionCell
version={row.original.version}
os={row.original.os}
serial={row.original.serial_number}
/>
),
},
{
@@ -260,31 +256,29 @@ export default function PeersTable({
}
};
const [showBrowserPeers, setShowBrowserPeers] = useState(false);
const [showBrowserPeers, setShowBrowserPeers] = useState(false);
const withBrowserPeers = useCallback(
(condition: boolean) =>
peers?.filter((peer) =>
condition
? peer.kernel_version === "wasm"
: peer.kernel_version !== "wasm",
) ?? [],
[peers],
);
const withBrowserPeers = useCallback(
(condition: boolean) =>
peers?.filter((peer) =>
condition ? trim(peer.os) === "js" : trim(peer.os) !== "js",
) ?? [],
[peers],
);
const browserPeers = useMemo(() => {
return withBrowserPeers(true);
}, [withBrowserPeers]);
const browserPeers = useMemo(() => {
return withBrowserPeers(true);
}, [withBrowserPeers]);
const regularPeers = useMemo(() => {
return withBrowserPeers(false);
}, [withBrowserPeers]);
const regularPeers = useMemo(() => {
return withBrowserPeers(false);
}, [withBrowserPeers]);
useEffect(() => {
if (showBrowserPeers && browserPeers?.length === 0) {
setShowBrowserPeers(false);
}
}, [showBrowserPeers, browserPeers]);
useEffect(() => {
if (showBrowserPeers && browserPeers?.length === 0) {
setShowBrowserPeers(false);
}
}, [showBrowserPeers, browserPeers]);
return (
<>
@@ -319,35 +313,7 @@ export default function PeersTable({
os: false,
}}
isLoading={isLoading}
getStartedCard={
<GetStartedTest
icon={
<SquareIcon
icon={<PeerIcon className={"fill-nb-gray-200"} size={20} />}
color={"gray"}
size={"large"}
/>
}
title={"Get Started with NetBird"}
description={
"It looks like you don't have any connected machines.\n" +
"Get started by adding one to your network."
}
button={<AddPeerButton />}
learnMore={
<>
Learn more in our{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/getting-started"}
target={"_blank"}
>
Getting Started Guide
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
}
getStartedCard={<NoPeersGettingStarted showBackground={true} />}
rightSide={() => <>{peers && peers.length > 0 && <AddPeerButton />}</>}
>
{(table) => (
@@ -516,27 +482,27 @@ export default function PeersTable({
/>
)}
{browserPeers?.length > 0 && (
<FullTooltip
content={
<div className={"max-w-sm text-xs"}>
Show temporary peers created by the NetBird browser client.
These peers are ephemeral and will be deleted automatically
after a short period of time.
</div>
}
>
<Button
className={"h-[44px]"}
variant={showBrowserPeers ? "tertiary" : "secondary"}
onClick={() => {
setShowBrowserPeers(!showBrowserPeers);
}}
>
<MonitorDotIcon size={16} />
</Button>
</FullTooltip>
)}
{browserPeers?.length > 0 && (
<FullTooltip
content={
<div className={"max-w-sm text-xs"}>
Show temporary peers created by the NetBird browser client.
These peers are ephemeral and will be deleted automatically
after a short period of time.
</div>
}
>
<Button
className={"h-[44px]"}
variant={showBrowserPeers ? "tertiary" : "secondary"}
onClick={() => {
setShowBrowserPeers(!showBrowserPeers);
}}
>
<MonitorDotIcon size={16} />
</Button>
</FullTooltip>
)}
<DataTableRefreshButton
isDisabled={peers?.length == 0}

View File

@@ -37,7 +37,8 @@ export const RDPButton = ({ peer, isDropdown = false }: Props) => {
<>
<div>
<RDPTooltip
disabled={!disabled}
//disabled={!disabled}
disabled={true}
hasPermission={hasPermission}
side={isDropdown ? "left" : "top"}
>

View File

@@ -41,7 +41,8 @@ export const SSHButton = ({ peer, isDropdown = false }: Props) => {
)}
<div>
<SSHTooltip
disabled={!disabled}
//disabled={!disabled}
disabled={true}
hasPermission={hasPermission}
side={isDropdown ? "left" : "top"}
>

View File

@@ -1,19 +1,19 @@
import { useApiCall } from "@utils/api";
import loadConfig from "@utils/config";
import { getBrowserInfo } from "@utils/helpers";
import { generateKeypair } from "@utils/wireguard";
import { trim } from "lodash";
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { IronRDPInputHandler } from "@/modules/remote-access/rdp/ironrdp-input-handler";
import { IronRDPWASMBridge } from "@/modules/remote-access/rdp/ironrdp-wasm-bridge";
import { RDPCertificateHandler } from "@/modules/remote-access/rdp/rdp-certificate-handler";
import { installWebSocketProxy } from "@/modules/remote-access/rdp/websocket-proxy";
import { useApiCall } from "@utils/api";
import { generateKeypair } from "@utils/wireguard";
import { getBrowserInfo } from "@utils/helpers";
import { trim } from "lodash";
const config = loadConfig();
const WASM_CONFIG = {
SCRIPT_PATH: "/wasm_exec.js",
WASM_PATH: "/netbird.wasm",
WASM_PATH: "https://pkgs.netbird.io/wasm/client",
INIT_TIMEOUT: 10000,
RETRY_DELAY: 100,
} as const;

View File

@@ -0,0 +1,52 @@
import Button from "@components/Button";
import { NotificationCountBadge } from "@components/ui/NotificationCountBadge";
import { Table } from "@tanstack/react-table";
import * as React from "react";
import { useEffect } from "react";
type Props<T> = {
table: Table<T>;
data?: T[];
count?: number;
};
export const PendingApprovalFilter = <T,>({ table, data, count }: Props<T>) => {
// Reset filter if there are no pending approvals
useEffect(() => {
if (
count == 0 &&
table.getColumn("approval_required")?.getFilterValue() === true
) {
table.setColumnFilters([]);
}
}, [count, table]);
if (!count) return;
return (
<Button
disabled={data?.length == 0}
onClick={() => {
table.setPageIndex(0);
let current =
table.getColumn("approval_required")?.getFilterValue() === undefined
? true
: undefined;
table.setColumnFilters([
{
id: "approval_required",
value: current,
},
]);
}}
variant={
table.getColumn("approval_required")?.getFilterValue() === true
? "tertiary"
: "secondary"
}
>
Pending Approvals
<NotificationCountBadge count={count} />
</Button>
);
};

View File

@@ -20,6 +20,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { User } from "@/interfaces/User";
import LastTimeRow from "@/modules/common-table-rows/LastTimeRow";
import { PendingApprovalFilter } from "@/modules/users/PendingApprovalFilter";
import UserActionCell from "@/modules/users/table-cells/UserActionCell";
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
import UserGroupCell from "@/modules/users/table-cells/UserGroupCell";
@@ -134,8 +135,6 @@ export default function UsersTable({
);
const router = useRouter();
const pendingApprovalCount =
users?.filter((u) => u.pending_approval).length || 0;
return (
<DataTable
@@ -196,44 +195,13 @@ export default function UsersTable({
)}
>
{(table) => {
if (
pendingApprovalCount == 0 &&
table.getColumn("approval_required")?.getFilterValue() === true
) {
table.setColumnFilters([]);
}
return (
<>
{pendingApprovalCount > 0 && (
<Button
disabled={users?.length == 0}
onClick={() => {
table.setPageIndex(0);
let current =
table.getColumn("approval_required")?.getFilterValue() ===
undefined
? true
: undefined;
table.setColumnFilters([
{
id: "approval_required",
value: current,
},
]);
}}
variant={
table.getColumn("approval_required")?.getFilterValue() ===
true
? "tertiary"
: "secondary"
}
>
Pending Approvals
<NotificationCountBadge count={pendingApprovalCount} />
</Button>
)}
<PendingApprovalFilter
table={table}
data={users}
count={users?.filter((u) => u?.pending_approval)?.length}
/>
<DataTableRowsPerPage table={table} disabled={users?.length == 0} />
<DataTableRefreshButton
isDisabled={users?.length == 0}

View File

@@ -215,7 +215,7 @@ export function useApiErrorHandling(ignoreError = false) {
const { login } = useOidc();
const currentPath = usePathname();
const { setError } = useErrorBoundary();
if (ignoreError)
return (err: ErrorResponse) => {
console.log(err);
@@ -232,21 +232,22 @@ export function useApiErrorHandling(ignoreError = false) {
if (err.code == 401 && err.message == "token invalid") {
setError(err);
}
// Handle user blocked/pending approval responses
if (err.code == 403 && (
err.message?.toLowerCase().includes("blocked") ||
err.message?.toLowerCase().includes("pending")
)) {
if (
err.code == 403 &&
(err.message?.toLowerCase().includes("blocked") ||
err.message?.toLowerCase().includes("pending"))
) {
const params = new URLSearchParams({
code: err.code.toString(),
message: encodeURIComponent(err.message),
type: "user-status"
type: "user-status",
});
window.location.href = `/error?${params.toString()}`;
return Promise.reject(err);
}
if (err.code == 500 && err.message == "internal server error") {
setError(err);
}