From 831673d0d6f4471fda9cc730f85872d94f382d30 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Fri, 3 Oct 2025 14:37:11 +0200 Subject: [PATCH] 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 --- .github/workflows/build_and_push.yml | 3 +- src/app/(dashboard)/peer/page.tsx | 35 +- src/app/not-found.tsx | 3 +- src/auth/OIDCProvider.tsx | 2 + src/components/Button.tsx | 2 +- src/components/FullTooltip.tsx | 7 +- src/components/ListItem.tsx | 29 ++ src/components/NoPeersGettingStarted.tsx | 44 +++ src/components/PeerGroupSelector.tsx | 352 +++++++++--------- src/components/SegmentedTabs.tsx | 3 + src/components/select/SelectDropdown.tsx | 124 ++++-- .../select/SelectDropdownSearchInput.tsx | 13 +- src/components/ui/GetStartedTest.tsx | 42 ++- src/components/ui/GroupBadgeIcon.tsx | 13 +- src/contexts/UsersProvider.tsx | 1 - src/hooks/useOperatingSystem.ts | 1 + src/interfaces/Peer.ts | 1 + .../table/AccessControlTable.tsx | 230 ++++++------ .../access-control/useAccessControl.ts | 4 +- src/modules/activity/ActivityDescription.tsx | 14 + src/modules/activity/utils.ts | 1 + .../misc/NetworkInformationSquare.tsx | 6 +- .../peers/PeerAddressTooltipContent.tsx | 29 +- src/modules/peers/PeerConnectButton.tsx | 59 +-- src/modules/peers/PeerNameCell.tsx | 16 +- src/modules/peers/PeerVersionCell.tsx | 7 +- src/modules/peers/PeersTable.tsx | 152 +++----- src/modules/remote-access/rdp/RDPButton.tsx | 3 +- src/modules/remote-access/ssh/SSHButton.tsx | 3 +- src/modules/remote-access/useNetBirdClient.ts | 10 +- src/modules/users/PendingApprovalFilter.tsx | 52 +++ src/modules/users/UsersTable.tsx | 44 +-- src/utils/api.tsx | 17 +- 33 files changed, 729 insertions(+), 593 deletions(-) create mode 100644 src/components/ListItem.tsx create mode 100644 src/components/NoPeersGettingStarted.tsx create mode 100644 src/modules/users/PendingApprovalFilter.tsx diff --git a/.github/workflows/build_and_push.yml b/.github/workflows/build_and_push.yml index 20b9dfd..b7b2a31 100644 --- a/.github/workflows/build_and_push.yml +++ b/.github/workflows/build_and_push.yml @@ -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 }} diff --git a/src/app/(dashboard)/peer/page.tsx b/src/app/(dashboard)/peer/page.tsx index 59f36b9..54887d2 100644 --- a/src/app/(dashboard)/peer/page.tsx +++ b/src/app/(dashboard)/peer/page.tsx @@ -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 = () => { /> - {/* Remote Access Buttons */} -
- - Connect directly to this peer via SSH or RDP. -
- - -
+ {/* Remote Access Buttons */} +
+ + Connect directly to this peer via SSH or RDP. +
+ +
+
{permission.groups.read && (
@@ -582,6 +582,23 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) { /> )} + {peer.created_at && ( + + + Registered on + + } + value={ + dayjs(peer.created_at).format("D MMMM, YYYY [at] h:mm A") + + " (" + + dayjs().to(peer.created_at) + + ")" + } + /> + )} + diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index b2afb48..902c1e1 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -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 ; }; diff --git a/src/auth/OIDCProvider.tsx b/src/auth/OIDCProvider.tsx index 2473721..69175fb 100644 --- a/src/auth/OIDCProvider.tsx +++ b/src/auth/OIDCProvider.tsx @@ -58,6 +58,8 @@ export default function OIDCProvider({ children }: Props) { "utm_content", "utm_campaign", "hs_id", + "page", + "page_size", "user", "port", ]; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 0697128..4b0a359 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -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", diff --git a/src/components/FullTooltip.tsx b/src/components/FullTooltip.tsx index 1f60a52..c67d81b 100644 --- a/src/components/FullTooltip.tsx +++ b/src/components/FullTooltip.tsx @@ -25,6 +25,8 @@ type Props = { customOnOpenChange?: React.Dispatch>; 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 && ( { + return ( +
+
+ {icon} + {label} +
+
{value}
+
+ ); +}; diff --git a/src/components/NoPeersGettingStarted.tsx b/src/components/NoPeersGettingStarted.tsx new file mode 100644 index 0000000..dfb7360 --- /dev/null +++ b/src/components/NoPeersGettingStarted.tsx @@ -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 ( + } + 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={} + learnMore={ + <> + Learn more in our{" "} + + Getting Started Guide + + + + } + /> + ); +}; diff --git a/src/components/PeerGroupSelector.tsx b/src/components/PeerGroupSelector.tsx index 607faff..4510fa6 100644 --- a/src/components/PeerGroupSelector.tsx +++ b/src/components/PeerGroupSelector.tsx @@ -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) { const { data: resources, isLoading: isResourcesLoading } = useFetchApi< - NetworkResource[] + NetworkResource[] >("/networks/resources"); const { data: peers, isLoading: isPeersLoading } = - useFetchApi("/peers"); + useFetchApi("/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} > - +
- + {searchedGroupNotFound && ( @@ -453,8 +454,8 @@ export function PeerGroupSelector({ value={search} onClick={(e) => e.preventDefault()} > - - + + {search}
e.preventDefault()} >
- +
@@ -533,10 +534,10 @@ export function PeerGroupSelector({ "text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2" } > - + {peerCount} Peer(s)
) : ( @@ -568,17 +569,17 @@ export function PeerGroupSelector({ /> )} - {showPeers && ( - - - - )} + {showPeers && ( + + + + )} @@ -597,6 +598,7 @@ const TabTriggers = ({ showPeers?: boolean; }) => { if (!showResources && !showPeers) return null; + return ( - {showResources && ( - searchRef.current?.focus()} - > - - Resources - - )} + {showResources && ( + searchRef.current?.focus()} + > + + Resources + + )} - {showPeers && ( - searchRef.current?.focus()} - > - - Peers - - )} + {showPeers && ( + searchRef.current?.focus()} + > + + Peers + + )} ); }; @@ -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 ( -
- - - - -
- ); - } - - if (search != "" && filteredItems.length == 0) { - return ( - - There are no peers matching your search. Please try a different search - term. - - ); - } - - if (search == "" && filteredItems.length == 0) { - return ( - - There are no peers available yet.
- Go to Peers to add some peers. -
- ); - } + useEffect(() => { + setSearch(search); + }, [search, setSearch]); + if (isLoading) { return ( - - { - if (!res?.id) return; - - return ( - -
- { - e.preventDefault(); - }} - > - - - -
- -
-
- {res.ip} - -
-
-
- ); - }} - /> -
+
+ + + + +
); -}; \ No newline at end of file + } + + if (search != "" && filteredItems.length == 0) { + return ( + + There are no peers matching your search. Please try a different search + term. + + ); + } + + if (search == "" && filteredItems.length == 0) { + return ( + + There are no peers available yet.
+ Go to Peers to add some peers. +
+ ); + } + + return ( + + { + if (!res?.id) return; + + return ( + +
+ { + e.preventDefault(); + }} + > + + + +
+ +
+
+ {res.ip} + +
+
+
+ ); + }} + /> +
+ ); +}; diff --git a/src/components/SegmentedTabs.tsx b/src/components/SegmentedTabs.tsx index 35e386b..76e080a 100644 --- a/src/components/SegmentedTabs.tsx +++ b/src/components/SegmentedTabs.tsx @@ -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} > diff --git a/src/components/select/SelectDropdown.tsx b/src/components/select/SelectDropdown.tsx index 64c19d7..50b3427 100644 --- a/src/components/select/SelectDropdown.tsx +++ b/src/components/select/SelectDropdown.tsx @@ -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) { const [inputRef, { width }] = useElementSize(); @@ -79,6 +87,46 @@ export function SelectDropdown({ }); }, [options, debouncedSearch]); + const Loading = () => { + return ( +
+ + +
+ ); + }; + + const SelectedItem = () => { + return ( +
+ {selected?.icon && } +
+ {selected?.label} +
+
+ ); + }; + + const PlaceholderItem = () => { + return ( +
+
+ {placeholder} +
+
+ ); + }; + return ( - - + + )} -
+
{filteredItems.map((option) => ( ))}
@@ -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(null); @@ -221,13 +256,20 @@ const SelectDropdownItem = ({ >
{option.icon && } -
+
{option.label}
{showValue && (
- + {option.value}
diff --git a/src/components/select/SelectDropdownSearchInput.tsx b/src/components/select/SelectDropdownSearchInput.tsx index 2e408b5..d49b266 100644 --- a/src/components/select/SelectDropdownSearchInput.tsx +++ b/src/components/select/SelectDropdownSearchInput.tsx @@ -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(
-
-
- -
-
+
); }, diff --git a/src/components/ui/GetStartedTest.tsx b/src/components/ui/GetStartedTest.tsx index e9c52dd..de61e6f 100644 --- a/src/components/ui/GetStartedTest.tsx +++ b/src/components/ui/GetStartedTest.tsx @@ -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 (
-
-
-
- - - - - -
-
+ {showBackground && ( + <> +
+
+
+ + + + + +
+
+ + )}
diff --git a/src/components/ui/GroupBadgeIcon.tsx b/src/components/ui/GroupBadgeIcon.tsx index ac0192c..0e4065b 100644 --- a/src/components/ui/GroupBadgeIcon.tsx +++ b/src/components/ui/GroupBadgeIcon.tsx @@ -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 ; + return ; if (isAzureGroup) - return ; - if (isOktaGroup) return ; - if (isJWTGroup) return ; + return ; + if (isOktaGroup) + return ; + if (isJWTGroup) return ; - return ; + return ; }; diff --git a/src/contexts/UsersProvider.tsx b/src/contexts/UsersProvider.tsx index 6945eca..f6bd17c 100644 --- a/src/contexts/UsersProvider.tsx +++ b/src/contexts/UsersProvider.tsx @@ -62,7 +62,6 @@ const UserProfileProvider = ({ children }: Props) => { } }, [user, error, users, isLoading, isAllUsersLoading]); - const data = useMemo(() => { return { loggedInUser, diff --git a/src/hooks/useOperatingSystem.ts b/src/hooks/useOperatingSystem.ts index 1a8e6a5..5de2856 100644 --- a/src/hooks/useOperatingSystem.ts +++ b/src/hooks/useOperatingSystem.ts @@ -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")) diff --git a/src/interfaces/Peer.ts b/src/interfaces/Peer.ts index 5c1bfb2..74f5577 100644 --- a/src/interfaces/Peer.ts +++ b/src/interfaces/Peer.ts @@ -6,6 +6,7 @@ export interface Peer { name: string; ip: string; connected: boolean; + created_at?: Date; last_seen: Date; os: string; version: string; diff --git a/src/modules/access-control/table/AccessControlTable.tsx b/src/modules/access-control/table/AccessControlTable.tsx index 0d43d04..a943dd1 100644 --- a/src/modules/access-control/table/AccessControlTable.tsx +++ b/src/modules/access-control/table/AccessControlTable.tsx @@ -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(); 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 ( - <> - - { - table.setPageIndex(0); - table.getColumn("enabled")?.setFilterValue(undefined); - }} - disabled={policies?.length == 0} - variant={ - table.getColumn("enabled")?.getFilterValue() === undefined - ? "tertiary" - : "secondary" - } - > - All - - { - table.setPageIndex(0); - table.getColumn("enabled")?.setFilterValue(true); - }} - disabled={policies?.length == 0} - variant={ - table.getColumn("enabled")?.getFilterValue() === true - ? "tertiary" - : "secondary" - } - > - Active - - { - table.setPageIndex(0); - table.getColumn("enabled")?.setFilterValue(false); - }} - disabled={policies?.length == 0} - variant={ - table.getColumn("enabled")?.getFilterValue() === false - ? "tertiary" - : "secondary" - } - > - Inactive - - - + {(table) => { + return ( + <> + + { + table.setPageIndex(0); + table.getColumn("enabled")?.setFilterValue(undefined); + }} + disabled={policies?.length == 0} + variant={ + table.getColumn("enabled")?.getFilterValue() === undefined + ? "tertiary" + : "secondary" + } + > + All + + { + table.setPageIndex(0); + table.getColumn("enabled")?.setFilterValue(true); + }} + disabled={policies?.length == 0} + variant={ + table.getColumn("enabled")?.getFilterValue() === true + ? "tertiary" + : "secondary" + } + > + Active + + { + table.setPageIndex(0); + table.getColumn("enabled")?.setFilterValue(false); + }} + disabled={policies?.length == 0} + variant={ + table.getColumn("enabled")?.getFilterValue() === false + ? "tertiary" + : "secondary" + } + > + Inactive + + + - {tempPolicies?.length > 0 && ( - - Show temporary policies created by the NetBird browser - client. These policies are ephemeral and will be - deleted automatically after a short period of time. -
- } - > - - - )} + {tempPolicies?.length > 0 && ( + + Show temporary policies created by the NetBird browser + client. These policies are ephemeral and will be deleted + automatically after a short period of time. +
+ } + > + + + )} - { - mutate("/policies").then(); - mutate("/groups").then(); - }} - /> - - ); - }} + { + mutate("/policies").then(); + mutate("/groups").then(); + }} + /> + + ); + }} ); -} \ No newline at end of file +} diff --git a/src/modules/access-control/useAccessControl.ts b/src/modules/access-control/useAccessControl.ts index 60db972..2e7d515 100644 --- a/src/modules/access-control/useAccessControl.ts +++ b/src/modules/access-control/useAccessControl.ts @@ -260,9 +260,9 @@ export const useAccessControl = ({ updatePolicy( policy, policyObj, - () => { + (p) => { mutate("/policies"); - onSuccess && onSuccess(policy); + onSuccess && onSuccess(p); }, "The policy was successfully saved", ); diff --git a/src/modules/activity/ActivityDescription.tsx b/src/modules/activity/ActivityDescription.tsx index 790b02e..a1ee0c1 100644 --- a/src/modules/activity/ActivityDescription.tsx +++ b/src/modules/activity/ActivityDescription.tsx @@ -677,6 +677,20 @@ export default function ActivityDescription({ event }: Props) {
); + if (event.activity_code == "account.settings.extra.flow.group.remove") + return ( +
+ Limit traffic event group {m.group_name} removed +
+ ); + + if (event.activity_code == "account.settings.extra.flow.group.add") + return ( +
+ Limit traffic event group {m.group_name} added +
+ ); + return (
{event.activity} diff --git a/src/modules/activity/utils.ts b/src/modules/activity/utils.ts index c1890da..9986c7d 100644 --- a/src/modules/activity/utils.ts +++ b/src/modules/activity/utils.ts @@ -18,6 +18,7 @@ const ACTION_COLOR_MAPPING: Record = { // Error actions delete: ActionStatus.ERROR, revoke: ActionStatus.ERROR, + remove: ActionStatus.ERROR, block: ActionStatus.ERROR, reject: ActionStatus.ERROR, diff --git a/src/modules/networks/misc/NetworkInformationSquare.tsx b/src/modules/networks/misc/NetworkInformationSquare.tsx index 4fd75c0..24bf7c5 100644 --- a/src/modules/networks/misc/NetworkInformationSquare.tsx +++ b/src/modules/networks/misc/NetworkInformationSquare.tsx @@ -20,9 +20,9 @@ export const NetworkInformationSquare = ({ return (
); }; - -const ListItem = ({ - icon, - label, - value, - className, -}: { - icon: React.ReactNode; - label: string; - value: string | React.ReactNode; - className?: string; -}) => { - return ( -
-
- {icon} - {label} -
-
{value}
-
- ); -}; diff --git a/src/modules/peers/PeerConnectButton.tsx b/src/modules/peers/PeerConnectButton.tsx index 5717ea3..c0e148c 100644 --- a/src/modules/peers/PeerConnectButton.tsx +++ b/src/modules/peers/PeerConnectButton.tsx @@ -22,35 +22,44 @@ export const PeerConnectButton = () => { if (isMobile) return; return isConnected ? ( - <> - - { - e.stopPropagation(); - e.preventDefault(); - }} - > -
- -
-
- - - - -
- + // <> + // + // { + // e.stopPropagation(); + // e.preventDefault(); + // }} + // > + //
+ // + //
+ //
+ // + // + // + // + //
+ // + + Connecting via SSH or RDP is coming soon. +
+ } + > + + ) : ( - Connecting via SSH or RDP is only available when the peer is online. + Connecting via SSH or RDP is coming soon.
} > diff --git a/src/modules/peers/PeerNameCell.tsx b/src/modules/peers/PeerNameCell.tsx index 11441b4..4fe2475 100644 --- a/src/modules/peers/PeerNameCell.tsx +++ b/src/modules/peers/PeerNameCell.tsx @@ -42,14 +42,14 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) { active={peer.connected} text={peer.name} additionalInfo={ - isOwnerOrAdmin && ( - <> - - - - - - ) + isOwnerOrAdmin && ( + <> + + + + + + ) } >
diff --git a/src/modules/peers/PeerVersionCell.tsx b/src/modules/peers/PeerVersionCell.tsx index 0ac6d24..154a868 100644 --- a/src/modules/peers/PeerVersionCell.tsx +++ b/src/modules/peers/PeerVersionCell.tsx @@ -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 ; }, []); + const isWasmClient = trim(os) === "js"; + return (
{updateAvailable ? ( @@ -111,7 +114,7 @@ export default function PeerVersionCell({ version, os, serial }: Props) { > - {os} + {isWasmClient ? "Web Client" : os}
)} diff --git a/src/modules/peers/PeersTable.tsx b/src/modules/peers/PeersTable.tsx index 78237db..69735a7 100644 --- a/src/modules/peers/PeersTable.tsx +++ b/src/modules/peers/PeersTable.tsx @@ -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[] = [ { @@ -76,14 +72,14 @@ const PeersTableColumns: ColumnDef[] = [ cell: ({ row }) => , }, { - id: "connect", - accessorKey: "id", - header: "", - cell: ({ row }) => ( - - - - ), + id: "connect", + accessorKey: "id", + header: "", + cell: ({ row }) => ( + + + + ), }, { id: "approval_required", @@ -170,11 +166,11 @@ const PeersTableColumns: ColumnDef[] = [ return Version; }, cell: ({ row }) => ( - + ), }, { @@ -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={ - } - 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={} - learnMore={ - <> - Learn more in our{" "} - - Getting Started Guide - - - - } - /> - } + getStartedCard={} rightSide={() => <>{peers && peers.length > 0 && }} > {(table) => ( @@ -516,27 +482,27 @@ export default function PeersTable({ /> )} - {browserPeers?.length > 0 && ( - - Show temporary peers created by the NetBird browser client. - These peers are ephemeral and will be deleted automatically - after a short period of time. -
- } - > - - - )} + {browserPeers?.length > 0 && ( + + Show temporary peers created by the NetBird browser client. + These peers are ephemeral and will be deleted automatically + after a short period of time. +
+ } + > + + + )} { <>
diff --git a/src/modules/remote-access/ssh/SSHButton.tsx b/src/modules/remote-access/ssh/SSHButton.tsx index cef1514..032ce20 100644 --- a/src/modules/remote-access/ssh/SSHButton.tsx +++ b/src/modules/remote-access/ssh/SSHButton.tsx @@ -41,7 +41,8 @@ export const SSHButton = ({ peer, isDropdown = false }: Props) => { )}
diff --git a/src/modules/remote-access/useNetBirdClient.ts b/src/modules/remote-access/useNetBirdClient.ts index b535556..08c9090 100644 --- a/src/modules/remote-access/useNetBirdClient.ts +++ b/src/modules/remote-access/useNetBirdClient.ts @@ -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; diff --git a/src/modules/users/PendingApprovalFilter.tsx b/src/modules/users/PendingApprovalFilter.tsx new file mode 100644 index 0000000..267ab29 --- /dev/null +++ b/src/modules/users/PendingApprovalFilter.tsx @@ -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 = { + table: Table; + data?: T[]; + count?: number; +}; + +export const PendingApprovalFilter = ({ table, data, count }: Props) => { + // 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 ( + + ); +}; diff --git a/src/modules/users/UsersTable.tsx b/src/modules/users/UsersTable.tsx index 35858bf..efa68a8 100644 --- a/src/modules/users/UsersTable.tsx +++ b/src/modules/users/UsersTable.tsx @@ -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 ( {(table) => { - if ( - pendingApprovalCount == 0 && - table.getColumn("approval_required")?.getFilterValue() === true - ) { - table.setColumnFilters([]); - } - return ( <> - {pendingApprovalCount > 0 && ( - - )} + u?.pending_approval)?.length} + /> { 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); }