diff --git a/src/app/(dashboard)/peer/page.tsx b/src/app/(dashboard)/peer/page.tsx index bda0bac..f830f7a 100644 --- a/src/app/(dashboard)/peer/page.tsx +++ b/src/app/(dashboard)/peer/page.tsx @@ -23,6 +23,7 @@ import Separator from "@components/Separator"; import FullScreenLoading from "@components/ui/FullScreenLoading"; import LoginExpiredBadge from "@components/ui/LoginExpiredBadge"; import TextWithTooltip from "@components/ui/TextWithTooltip"; +import useRedirect from "@hooks/useRedirect"; import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react"; import useFetchApi from "@utils/api"; import dayjs from "dayjs"; @@ -66,8 +67,11 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable"; export default function PeerPage() { const queryParameter = useSearchParams(); const peerId = queryParameter.get("id"); - const { data: peer } = useFetchApi("/peers/" + peerId); - return peer ? ( + const { data: peer, isLoading } = useFetchApi("/peers/" + peerId, true); + + useRedirect("/peers", false, !peerId); + + return peer && !isLoading ? ( diff --git a/src/app/(dashboard)/team/user/page.tsx b/src/app/(dashboard)/team/user/page.tsx index df56117..62320f1 100644 --- a/src/app/(dashboard)/team/user/page.tsx +++ b/src/app/(dashboard)/team/user/page.tsx @@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph"; import { PeerGroupSelector } from "@components/PeerGroupSelector"; import Separator from "@components/Separator"; import FullScreenLoading from "@components/ui/FullScreenLoading"; +import useRedirect from "@hooks/useRedirect"; import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react"; import useFetchApi, { useApiCall } from "@utils/api"; import { generateColorFromString } from "@utils/helpers"; @@ -42,6 +43,8 @@ export default function UserPage() { return users?.find((u) => u.id === userId); }, [users, userId]); + useRedirect("/team/users", false, !userId); + return !isLoading && user ? ( ) : ( diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 58f7f98..b2afb48 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -35,6 +35,6 @@ export default function NotFound() { } const Redirect = ({ url, queryParams }: Props) => { - useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`)); + useRedirect("/peers" + (queryParams && `?${queryParams}`)); return ; }; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index e2e5230..31649ca 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -13,7 +13,6 @@ export interface InputProps icon?: React.ReactNode; error?: string; errorTooltip?: boolean; - errorTooltipPosition?: "top" | "top-right"; } const inputVariants = cva("", { @@ -50,7 +49,6 @@ const Input = React.forwardRef( maxWidthClass = "", error, errorTooltip = false, - errorTooltipPosition = "top", ...props }, ref, @@ -107,12 +105,9 @@ const Input = React.forwardRef( {error && errorTooltip && (
(
} interactive={false} - align={errorTooltipPosition == "top" ? "center" : "end"} + align={"center"} side={"top"} keepOpen={true} > diff --git a/src/hooks/useRedirect.tsx b/src/hooks/useRedirect.tsx index 51dd557..4be09fe 100644 --- a/src/hooks/useRedirect.tsx +++ b/src/hooks/useRedirect.tsx @@ -1,8 +1,9 @@ import loadConfig from "@utils/config"; import { usePathname, useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; const config = loadConfig(); + export const useRedirect = ( url: string, replace: boolean = false, @@ -10,24 +11,43 @@ export const useRedirect = ( ) => { const router = useRouter(); const currentPath = usePathname(); - const callBackUrls = [config.redirectURI, config.silentRedirectURI]; + const callBackUrls = useRef([config.redirectURI, config.silentRedirectURI]); + const isRedirecting = useRef(false); + const intervalRef = useRef(null); useEffect(() => { - if (!enable) return; - if (callBackUrls.includes(url)) return; // Don't redirect to the callback urls to avoid infinite loop - if (url === currentPath) return; // Don't redirect to the current page + // If redirect is disabled or the url is already in the callback urls or the url is the current path then do not redirect + if (!enable || callBackUrls.current.includes(url) || url === currentPath) + return; - const redirect = replace ? router.replace : router.push; // Replace the current history or add a new one + const performRedirect = () => { + if (!isRedirecting.current) { + isRedirecting.current = true; + router.refresh(); + if (replace) { + router.replace(url); + } else { + router.push(url); + } + isRedirecting.current = false; + } + }; - router.refresh(); - redirect(url); + performRedirect(); - // Timer in case the user has his browser tab open but not focused - const interval = setInterval(() => { - router.refresh(); - redirect(url); - }, 1000); + // Try to redirect after 1.25 seconds if for whatever reason the redirect did not happen (network change, browser tab open but not focused etc.) + intervalRef.current = setInterval(() => { + if (!isRedirecting.current) { + performRedirect(); + } + }, 1250); - return () => clearInterval(interval); - }, [replace, router, url, enable]); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [replace, router, url, enable, currentPath]); }; + +export default useRedirect; diff --git a/src/modules/activity/ActivityDescription.tsx b/src/modules/activity/ActivityDescription.tsx index 78b8902..5698d39 100644 --- a/src/modules/activity/ActivityDescription.tsx +++ b/src/modules/activity/ActivityDescription.tsx @@ -3,7 +3,11 @@ import { Label } from "@components/Label"; import { IconInfoCircle } from "@tabler/icons-react"; import { cn } from "@utils/helpers"; import { isLocalDev, isProduction } from "@utils/netbird"; +import { isEmpty } from "lodash"; +import { GlobeIcon } from "lucide-react"; import React, { useMemo } from "react"; +import RoundedFlag from "@/assets/countries/RoundedFlag"; +import { useCountries } from "@/contexts/CountryProvider"; import { ActivityEvent } from "@/interfaces/ActivityEvent"; type Props = { @@ -54,7 +58,8 @@ export default function ActivityDescription({ event }: Props) { if (event.activity_code == "setupkey.peer.add") return (
- Peer {m.name} with ip {m.ip} was added + Peer {m.name} was added + with the NetBird IP {m.ip}
); @@ -144,21 +149,24 @@ export default function ActivityDescription({ event }: Props) { if (event.activity_code == "user.peer.delete") return (
- Peer {m.name} with ip {m.ip} was deleted + Peer {m.name} with + NetBird IP {m.ip} was deleted
); if (event.activity_code == "user.peer.add") return (
- Peer {m.name} with ip {m.ip} was added + Peer {m.name} was added + with the NetBird IP {m.ip}
); if (event.activity_code == "user.peer.update") return (
- Peer {m.name} with ip {m.ip} was updated + Peer {m.name} with + NetBird IP {m.ip} was updated
); @@ -252,15 +260,15 @@ export default function ActivityDescription({ event }: Props) { if (event.activity_code == "peer.group.delete") return (
- Group {m.group} was removed from the peer with the ip{" "} - {m.peer_ip} + Group {m.group} was removed from the peer with the + NetBird IP {m.peer_ip}
); if (event.activity_code == "peer.group.add") return (
- Group {m.group} was added to the peer with the ip{" "} + Group {m.group} was added to the peer with the NetBird IP{" "} {m.peer_ip}
); @@ -303,7 +311,7 @@ export default function ActivityDescription({ event }: Props) { if (event.activity_code == "peer.rename") return (
- Peer with the ip {m.ip} was renamed to{" "} + Peer with the NetBird IP {m.ip} was renamed to{" "} {m.name}
); @@ -311,7 +319,7 @@ export default function ActivityDescription({ event }: Props) { if (event.activity_code == "peer.approve") return (
- Peer with the ip {m.ip} was approved + Peer with the NetBird IP {m.ip} was approved
); @@ -559,7 +567,7 @@ function Value({ return children ? ( @@ -567,3 +575,40 @@ function Value({ ) : null; } + +function PeerConnectionInfo({ meta }: { meta: any }) { + const hasMeta = + !isEmpty(meta?.location_country_code) || + !isEmpty(meta?.location_connection_ip); + const { countries } = useCountries(); + + const countryText = useMemo(() => { + if (!countries) return "Unknown"; + const country = countries.find( + (c) => c.country_code === meta?.location_country_code, + ); + if (!country) return "Unknown"; + if (!meta?.location_city_name) return country.country_name; + return `${country.country_name}, ${meta?.location_city_name}`; + }, [countries, meta]); + + return hasMeta ? ( + <> + {" "} + from{" "} + {meta?.location_connection_ip && ( + {meta?.location_connection_ip} + )}{" "} + {meta?.location_country_code && ( + + {isEmpty(meta?.location_country_code) ? ( + + ) : ( + + )} + {countryText} + + )} + + ) : null; +} diff --git a/src/modules/exit-node/ExitNodeDropdownButton.tsx b/src/modules/exit-node/ExitNodeDropdownButton.tsx index 20ffa8a..c67c706 100644 --- a/src/modules/exit-node/ExitNodeDropdownButton.tsx +++ b/src/modules/exit-node/ExitNodeDropdownButton.tsx @@ -1,11 +1,13 @@ import { DropdownMenuItem } from "@components/DropdownMenu"; import { Modal } from "@components/modal/Modal"; import { getOperatingSystem } from "@hooks/useOperatingSystem"; -import { IconDirectionSign } from "@tabler/icons-react"; +import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react"; import * as React from "react"; import { useState } from "react"; +import RoutesProvider from "@/contexts/RoutesProvider"; import { OperatingSystem } from "@/interfaces/OperatingSystem"; import { Peer } from "@/interfaces/Peer"; +import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes"; import { RouteModalContent } from "@/modules/routes/RouteModal"; type Props = { @@ -15,24 +17,41 @@ type Props = { export const ExitNodeDropdownButton = ({ peer }: Props) => { const [modal, setModal] = useState(false); const isLinux = getOperatingSystem(peer.os) === OperatingSystem.LINUX; + const hasExitNodes = useHasExitNodes(peer); return isLinux ? ( <> setModal(true)}>
- -
- Add Exit Node -
+ {hasExitNodes ? ( + <> + +
+ Add Exit Node +
+ + ) : ( + <> + +
+ Setup Exit Node +
+ + )}
{modal && ( - setModal(false)} - peer={peer} - exitNode={true} - /> + + setModal(false)} + peer={peer} + exitNode={true} + /> + )} diff --git a/src/modules/routes/RouteTable.tsx b/src/modules/routes/RouteTable.tsx index 3764731..7d1ab22 100644 --- a/src/modules/routes/RouteTable.tsx +++ b/src/modules/routes/RouteTable.tsx @@ -23,6 +23,10 @@ export const RouteTableColumns: ColumnDef[] = [ sortingFn: "text", cell: ({ row }) => , }, + { + accessorKey: "description", + sortingFn: "text", + }, { accessorKey: "metric", header: ({ column }) => { @@ -122,6 +126,7 @@ export default function RouteTable({ row }: Props) { sorting={sorting} columnVisibility={{ group_names: false, + description: false, }} onRowClick={(row, cell) => { setCurrentRow(row.original); diff --git a/src/modules/setup-keys/SetupKeyKeyCell.tsx b/src/modules/setup-keys/SetupKeyKeyCell.tsx new file mode 100644 index 0000000..bb9fbd1 --- /dev/null +++ b/src/modules/setup-keys/SetupKeyKeyCell.tsx @@ -0,0 +1,16 @@ +import Badge from "@components/Badge"; +import React from "react"; + +type Props = { + text: string; +}; + +export default function SetupKeyKeyCell({ text }: Props) { + return ( +
+ + {text.substring(0, 5) + "****"} + +
+ ); +} diff --git a/src/modules/setup-keys/SetupKeyTypeCell.tsx b/src/modules/setup-keys/SetupKeyTypeCell.tsx new file mode 100644 index 0000000..63ef685 --- /dev/null +++ b/src/modules/setup-keys/SetupKeyTypeCell.tsx @@ -0,0 +1,24 @@ +import Badge from "@components/Badge"; +import { IconRepeat } from "@tabler/icons-react"; +import { Repeat1 } from "lucide-react"; + +type Props = { + reusable: boolean; +}; +export default function SetupKeyTypeCell({ reusable }: Props) { + return ( +
+ + {reusable ? ( + <> + Reusable + + ) : ( + <> + One-off + + )} + +
+ ); +}