Update redirect logic, fix exit node dropdown, fix route search

This commit is contained in:
Eduard Gert
2024-06-12 16:58:10 +02:00
parent 79164e9dd5
commit 0e553f3c83
10 changed files with 178 additions and 47 deletions

View File

@@ -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<Peer>("/peers/" + peerId);
return peer ? (
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
useRedirect("/peers", false, !peerId);
return peer && !isLoading ? (
<PeerProvider peer={peer}>
<PeerOverview />
</PeerProvider>

View File

@@ -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 ? (
<UserOverview user={user} />
) : (

View File

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

View File

@@ -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<HTMLInputElement, InputProps>(
maxWidthClass = "",
error,
errorTooltip = false,
errorTooltipPosition = "top",
...props
},
ref,
@@ -107,12 +105,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</div>
{error && errorTooltip && (
<div
className={cn(
errorTooltipPosition == "top" &&
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center",
errorTooltipPosition == "top-right" &&
"absolute -right-6 top-2 h-[0px] w-full flex items-center pr-3 justify-end",
)}
className={
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center"
}
>
<FullTooltip
content={
@@ -125,7 +120,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
</div>
}
interactive={false}
align={errorTooltipPosition == "top" ? "center" : "end"}
align={"center"}
side={"top"}
keepOpen={true}
>

View File

@@ -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<NodeJS.Timeout | null>(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;

View File

@@ -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 (
<div className={"inline"}>
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
with the NetBird IP <Value>{m.ip}</Value>
</div>
);
@@ -144,21 +149,24 @@ export default function ActivityDescription({ event }: Props) {
if (event.activity_code == "user.peer.delete")
return (
<div className={"inline"}>
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was deleted
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
NetBird IP <Value>{m.ip}</Value> was deleted
</div>
);
if (event.activity_code == "user.peer.add")
return (
<div className={"inline"}>
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
with the NetBird IP <Value>{m.ip}</Value>
</div>
);
if (event.activity_code == "user.peer.update")
return (
<div className={"inline"}>
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was updated
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
NetBird IP <Value>{m.ip}</Value> was updated
</div>
);
@@ -252,15 +260,15 @@ export default function ActivityDescription({ event }: Props) {
if (event.activity_code == "peer.group.delete")
return (
<div className={"inline"}>
Group <Value>{m.group}</Value> was removed from the peer with the ip{" "}
<Value>{m.peer_ip}</Value>
Group <Value>{m.group}</Value> was removed from the peer with the
NetBird IP <Value>{m.peer_ip}</Value>
</div>
);
if (event.activity_code == "peer.group.add")
return (
<div className={"inline"}>
Group <Value>{m.group}</Value> was added to the peer with the ip{" "}
Group <Value>{m.group}</Value> was added to the peer with the NetBird IP{" "}
<Value>{m.peer_ip}</Value>
</div>
);
@@ -303,7 +311,7 @@ export default function ActivityDescription({ event }: Props) {
if (event.activity_code == "peer.rename")
return (
<div className={"inline"}>
Peer with the ip <Value>{m.ip}</Value> was renamed to{" "}
Peer with the NetBird IP <Value>{m.ip}</Value> was renamed to{" "}
<Value>{m.name}</Value>
</div>
);
@@ -311,7 +319,7 @@ export default function ActivityDescription({ event }: Props) {
if (event.activity_code == "peer.approve")
return (
<div className={"inline"}>
Peer with the ip <Value>{m.ip}</Value> was approved
Peer with the NetBird IP <Value>{m.ip}</Value> was approved
</div>
);
@@ -559,7 +567,7 @@ function Value({
return children ? (
<span
className={cn(
"text-nb-gray-200 inline font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
"text-nb-gray-200 inline-flex gap-1 items-center max-h-[22px] font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
className,
)}
>
@@ -567,3 +575,40 @@ function Value({
</span>
) : 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 && (
<Value>{meta?.location_connection_ip}</Value>
)}{" "}
{meta?.location_country_code && (
<Value>
{isEmpty(meta?.location_country_code) ? (
<GlobeIcon size={9} className={"text-nb-gray-300"} />
) : (
<RoundedFlag country={meta?.location_country_code} size={9} />
)}
{countryText}
</Value>
)}
</>
) : null;
}

View File

@@ -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 ? (
<>
<DropdownMenuItem onClick={() => setModal(true)}>
<div className={"flex gap-3 items-center w-full"}>
<IconDirectionSign size={14} className={"shrink-0"} />
<div className={"flex justify-between items-center w-full"}>
Add Exit Node
</div>
{hasExitNodes ? (
<>
<IconCirclePlus size={14} className={"shrink-0"} />
<div className={"flex justify-between items-center w-full"}>
Add Exit Node
</div>
</>
) : (
<>
<IconDirectionSign
size={14}
className={"shrink-0 text-yellow-400"}
/>
<div className={"flex justify-between items-center w-full"}>
Setup Exit Node
</div>
</>
)}
</div>
</DropdownMenuItem>
<Modal open={modal} onOpenChange={setModal}>
{modal && (
<RouteModalContent
onSuccess={() => setModal(false)}
peer={peer}
exitNode={true}
/>
<RoutesProvider>
<RouteModalContent
onSuccess={() => setModal(false)}
peer={peer}
exitNode={true}
/>
</RoutesProvider>
)}
</Modal>
</>

View File

@@ -23,6 +23,10 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
sortingFn: "text",
cell: ({ row }) => <RoutePeerCell route={row.original} />,
},
{
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);

View File

@@ -0,0 +1,16 @@
import Badge from "@components/Badge";
import React from "react";
type Props = {
text: string;
};
export default function SetupKeyKeyCell({ text }: Props) {
return (
<div className={"flex"}>
<Badge variant={"gray"} className={"text-xs font-mono"}>
{text.substring(0, 5) + "****"}
</Badge>
</div>
);
}

View File

@@ -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 (
<div className={"flex"}>
<Badge className={"text-xs"} variant={"gray"}>
{reusable ? (
<>
<IconRepeat size={14} className={"text-green-400"} /> Reusable
</>
) : (
<>
<Repeat1 size={14} /> One-off
</>
)}
</Badge>
</div>
);
}