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); }