Skip to content

Commit

Permalink
Change exit node button, add indicator to peers table
Browse files Browse the repository at this point in the history
  • Loading branch information
heisbrot committed Apr 8, 2024
1 parent 6544724 commit fd1a15c
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 44 deletions.
4 changes: 3 additions & 1 deletion src/app/(dashboard)/peer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer";
import { AddExitNodeButton } from "@/modules/exit-node/AddExitNodeButton";
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
Expand Down Expand Up @@ -128,6 +129,7 @@ function PeerOverview() {
};

const { isUser } = useLoggedInUser();
const hasExitNodes = useHasExitNodes(peer);

return (
<PageContainer>
Expand Down Expand Up @@ -344,7 +346,7 @@ function PeerOverview() {
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div className={"gap-4 flex"}>
<AddExitNodeButton peer={peer} />
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
<AddRouteDropdownButton />
</div>
</div>
Expand Down
14 changes: 10 additions & 4 deletions src/components/modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface Props extends IconVariant {
description: string | React.ReactNode;
className?: string;
margin?: string;
truncate?: boolean;
}
export default function ModalHeader({
icon,
Expand All @@ -17,14 +18,19 @@ export default function ModalHeader({
color = "netbird",
className = "pb-6 px-8",
margin = "mt-0",
truncate = false,
}: Props) {
return (
<div className={className}>
<div className={"flex items-start gap-5 pr-10"}>
<div className={cn(className, "min-w-0")}>
<div className={"flex items-start gap-5 pr-10 min-w-0"}>
{icon && <SquareIcon color={color} icon={icon} />}
<div>
<div className={"min-w-0"}>
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
<Paragraph className={cn("text-sm", margin)}>{description}</Paragraph>
<Paragraph
className={cn("text-sm", margin, truncate && "!block truncate")}
>
{description}
</Paragraph>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/TextWithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function TextWithTooltip({
<div
className={"w-full min-w-0 inline-block"}
style={{
maxWidth: `${maxChars + 5}ch`,
maxWidth: `${maxChars - 2}ch`,
}}
>
<div className={cn(className, "truncate")}>{text}</div>
Expand Down
19 changes: 15 additions & 4 deletions src/modules/exit-node/AddExitNodeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Button from "@components/Button";
import { Modal } from "@components/modal/Modal";
import { IconCirclePlus } from "@tabler/icons-react";
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
import * as React from "react";
import { useState } from "react";
import { Peer } from "@/interfaces/Peer";
Expand All @@ -9,23 +9,34 @@ import { RouteModalContent } from "@/modules/routes/RouteModal";

type Props = {
peer?: Peer;
firstTime?: boolean;
};
export const AddExitNodeButton = ({ peer }: Props) => {
export const AddExitNodeButton = ({ peer, firstTime = false }: Props) => {
const [modal, setModal] = useState(false);

return (
<>
<ExitNodeHelpTooltip>
<Button variant={"secondary"} onClick={() => setModal(true)}>
<IconCirclePlus size={16} />
Add Exit Node
{!firstTime ? (
<>
<IconCirclePlus size={16} />
Add Exit Node
</>
) : (
<>
<IconDirectionSign size={16} className={"text-yellow-400"} />
Setup Exit Node
</>
)}
</Button>
</ExitNodeHelpTooltip>
<Modal open={modal} onOpenChange={setModal}>
{modal && (
<RouteModalContent
onSuccess={() => setModal(false)}
peer={peer}
isFirstExitNode={firstTime}
exitNode={true}
/>
)}
Expand Down
12 changes: 5 additions & 7 deletions src/modules/exit-node/ExitNodePeerIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import FullTooltip from "@components/FullTooltip";
import { IconDirectionSign } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import * as React from "react";
import { Peer } from "@/interfaces/Peer";
import { Route } from "@/interfaces/Route";
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";

type Props = {
peer: Peer;
};
export const ExitNodePeerIndicator = ({ peer }: Props) => {
const { data: routes } = useFetchApi<Route[]>(`/routes`);
const isExitNode = routes?.some((route) => route?.peer === peer.id);
const hasExitNode = useHasExitNodes(peer);

return isExitNode ? (
return hasExitNode ? (
<FullTooltip
content={
<div className={"text-xs max-w-xs"}>
This peer is used as an exit node. All internet traffic will be routed
through this peer.
This peer has an exit node. Traffic from the configured distribution
groups will be routed through this peer.
</div>
}
>
Expand Down
10 changes: 10 additions & 0 deletions src/modules/exit-node/useHasExitNodes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import useFetchApi from "@utils/api";
import { Peer } from "@/interfaces/Peer";
import { Route } from "@/interfaces/Route";

export const useHasExitNodes = (peer?: Peer) => {
const { data: routes } = useFetchApi<Route[]>(`/routes`);
return peer
? routes?.some((route) => route?.peer === peer.id) || false
: false;
};
11 changes: 11 additions & 0 deletions src/modules/peer/PeerRoutesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import PeerRouteActiveCell from "@/modules/peer/PeerRouteActiveCell";
import PeerRouteNameCell from "@/modules/peer/PeerRouteNameCell";
import PeerRouteNetworkCell from "@/modules/peer/PeerRouteNetworkCell";
import usePeerRoutes from "@/modules/peer/usePeerRoutes";
import RouteDistributionGroupsCell from "@/modules/routes/RouteDistributionGroupsCell";

type Props = {
peer: Peer;
Expand All @@ -35,6 +36,16 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
},
cell: ({ row }) => <PeerRouteNetworkCell route={row.original} />,
},
{
id: "groups",
accessorFn: (r) => r.groups?.length,
header: ({ column }) => {
return (
<DataTableHeader column={column}>Distribution Groups</DataTableHeader>
);
},
cell: ({ row }) => <RouteDistributionGroupsCell route={row.original} />,
},
{
id: "enabled",
accessorKey: "enabled",
Expand Down
88 changes: 61 additions & 27 deletions src/modules/routes/RouteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,35 @@ type ModalProps = {
onSuccess?: (route: Route) => void;
peer?: Peer;
exitNode?: boolean;
isFirstExitNode?: boolean;
};

export function RouteModalContent({ onSuccess, peer, exitNode }: ModalProps) {
export function RouteModalContent({
onSuccess,
peer,
exitNode,
isFirstExitNode = false,
}: ModalProps) {
const { createRoute } = useRoutes();
const [tab, setTab] = useState("network");

// General
/**
* Network Identifier, Description & Network Range
*/
const [networkIdentifier, setNetworkIdentifier] = useState(
exitNode ? (peer ? `Exit Node (${peer.name})` : "Exit Node") : "",
exitNode
? peer
? `Exit Node (${
peer.name.length > 25
? peer.name.substring(0, 25) + "..."
: peer.name
})`
: "Exit Node"
: "",
);
const [description, setDescription] = useState("");

// Network
const [networkRange, setNetworkRange] = useState(exitNode ? "0.0.0.0/0" : "");
const [routingPeer, setRoutingPeer] = useState<Peer | undefined>(peer);

const [
routingPeerGroups,
setRoutingPeerGroups,
Expand All @@ -88,29 +102,23 @@ export function RouteModalContent({ onSuccess, peer, exitNode }: ModalProps) {
initial: [],
});

/**
* Distribution Groups
*/
const [groups, setGroups, { getGroupsToUpdate }] = useGroupHelper({
initial: [],
});

// Additional Settings
/**
* Additional Settings
*/
const [enabled, setEnabled] = useState<boolean>(true);
const [metric, setMetric] = useState("9999");
const [masquerade, setMasquerade] = useState<boolean>(true);

// Validate CIDR
const cidrError = useMemo(() => {
if (networkRange == "") return "";
const validCIDR = cidr.isValidAddress(networkRange);
if (!validCIDR) return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
}, [networkRange]);

// Refs to manage focus on tab change
const networkRangeRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const [peerTab, setPeerTab] = useState("routing-peer");

// Create route
// TODO Refactor to avoid duplicate code
/**
* Create Route
*/
const createRouteHandler = async () => {
const g1 = getAllRoutingGroupsToUpdate();
const g2 = getGroupsToUpdate();
Expand Down Expand Up @@ -151,7 +159,25 @@ export function RouteModalContent({ onSuccess, peer, exitNode }: ModalProps) {
);
};

// Is button disabled
/**
* Refs to manage input focus on tab change
*/
const networkRangeRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const [peerTab, setPeerTab] = useState("routing-peer");

/**
* Validate CIDR Range
*/
const cidrError = useMemo(() => {
if (networkRange == "") return "";
const validCIDR = cidr.isValidAddress(networkRange);
if (!validCIDR) return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
}, [networkRange]);

/**
* Allow to create route only when all fields are filled
*/
const isDisabled = useMemo(() => {
return (
networkIdentifier == "" ||
Expand All @@ -169,8 +195,6 @@ export function RouteModalContent({ onSuccess, peer, exitNode }: ModalProps) {
groups,
]);

const [tab, setTab] = useState("network");

return (
<ModalContent maxWidthClass={"max-w-xl"}>
<ModalHeader
Expand All @@ -181,7 +205,14 @@ export function RouteModalContent({ onSuccess, peer, exitNode }: ModalProps) {
<NetworkRoutesIcon className={"fill-netbird"} />
)
}
title={exitNode ? "Add Exit Node" : "Create New Route"}
title={
exitNode
? isFirstExitNode
? "Setup Exit Node"
: "Add Exit Node"
: "Create New Route"
}
truncate={!!peer}
description={
exitNode
? peer
Expand Down Expand Up @@ -290,8 +321,11 @@ export function RouteModalContent({ onSuccess, peer, exitNode }: ModalProps) {
<div>
<Label>Distribution Groups</Label>
<HelpText>
Advertise this route to peers that belong to the following
groups
{exitNode
? peer
? `Route all internet traffic through this peer for the following groups`
: `Route all internet traffic through the peer(s) for the following groups`
: "Advertise this route to peers that belong to the following groups"}
</HelpText>
<PeerGroupSelector onChange={setGroups} values={groups} />
</div>
Expand Down

0 comments on commit fd1a15c

Please sign in to comment.