Compare commits

...
Author SHA1 Message Date
Eduard Gert 0e553f3c83 Update redirect logic, fix exit node dropdown, fix route search 2024-06-12 16:58:10 +02:00
Eduard Gert 79164e9dd5 Add process posture check (#378)
* Add process posture check

* Add support for separate linux and mac paths
2024-06-12 16:32:10 +02:00
Eduard Gert 5caeab118b UX changes for modals and refactoring (#380) 2024-05-08 14:42:04 +02:00
37 changed files with 1082 additions and 259 deletions

No files matched your search

+3
View File
@@ -5,6 +5,9 @@ const nextConfig = {
unoptimized: true,
},
reactStrictMode: false,
env: {
APP_ENV: process.env.APP_ENV || "production",
},
};
module.exports = nextConfig;
+6 -2
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>
+3
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} />
) : (
+33 -7
View File
@@ -1,14 +1,40 @@
"use client";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useLocalStorage } from "@hooks/useLocalStorage";
import { useRedirect } from "@hooks/useRedirect";
import { useEffect, useState } from "react";
type Props = {
url: string;
queryParams?: string;
};
export default function NotFound() {
const router = useRouter();
useEffect(() => {
router.push("/peers");
});
const [mounted, setMounted] = useState(false);
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
"netbird-query-params",
"",
);
const [queryParams, setQueryParams] = useState("");
return <FullScreenLoading />;
useEffect(() => {
setQueryParams(tempQueryParams);
setTempQueryParams("");
setMounted(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return mounted ? (
<Redirect
url={window?.location?.pathname || "/"}
queryParams={queryParams}
/>
) : (
<FullScreenLoading />
);
}
const Redirect = ({ url, queryParams }: Props) => {
useRedirect("/peers" + (queryParams && `?${queryParams}`));
return <FullScreenLoading />;
};
+2
View File
@@ -16,6 +16,8 @@ export default function CircleIcon({
return (
<span
style={{ width: size + "px", height: size + "px" }}
data-cy="circle-icon"
data-cy-status={active ? "active" : "inactive"}
className={cn(
"rounded-full",
active
+8 -2
View File
@@ -5,14 +5,16 @@ import React, { forwardRef } from "react";
type Props = {
children: React.ReactNode;
disabled?: boolean;
className?: string;
};
function ButtonGroup({ children, disabled }: Props) {
function ButtonGroup({ children, disabled, className }: Props) {
return (
<div
className={cn(
"rounded-lg border-[1px] dark:border-nb-gray-900 border-neutral-200 overflow-hidden flex items-center justify-center shrink-0 border-separate",
disabled ? "opacity-100 !border-nb-gray-900/20" : "",
className,
)}
>
{children}
@@ -21,7 +23,10 @@ function ButtonGroup({ children, disabled }: Props) {
}
const ButtonGroupButton = forwardRef(
({ ...props }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
(
{ className, ...props }: ButtonProps,
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
return (
<Button
ref={ref}
@@ -31,6 +36,7 @@ const ButtonGroupButton = forwardRef(
className={cn(
"first:border-l-0 last:border-r-0 border-t-0 border-b-0 h-[41px]",
"!py-2.5 !px-4",
className,
)}
/>
);
+1
View File
@@ -198,6 +198,7 @@ export function PeerGroupSelector({
<CommandList className={"w-full"}>
<div className={"relative"}>
<CommandInput
data-cy={"group-search-input"}
className={cn(
"min-h-[42px] w-full relative",
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
+11 -4
View File
@@ -21,12 +21,19 @@ function SegmentedTabs({ value, onChange, children }: Props) {
);
}
function List({ children }: { children: React.ReactNode }) {
function List({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<TabsList
className={
"bg-nb-gray-930/70 p-1.5 rounded-t-lg flex justify-center gap-1 border border-b-0 border-nb-gray-900"
}
className={cn(
"bg-nb-gray-930/70 p-1.5 rounded-t-lg flex justify-center gap-1 border border-b-0 border-nb-gray-900",
className,
)}
>
{children}
</TabsList>
+4 -1
View File
@@ -75,7 +75,10 @@ const ModalContent = React.forwardRef<
<>
{children}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
<DialogPrimitive.Close
data-cy={"modal-close"}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@@ -28,9 +28,10 @@ export function DataTableRowsPerPage<TData>({
role="combobox"
aria-expanded={open}
disabled={disabled}
data-cy={"rows-per-page"}
className="w-[200px] justify-between"
>
<RowsIcon size={15} className={"text-nb-gray-300"} />
<RowsIcon size={15} className={"text-nb-gray-300 shrink-0"} />
<div>
<span className={"text-white"}>
{table.getState().pagination.pageSize}
@@ -47,6 +48,7 @@ export function DataTableRowsPerPage<TData>({
<CommandItem
key={val}
value={val.toString()}
data-cy={`rows-per-page-value`}
onSelect={(currentValue) => {
table.setPageSize(Number(currentValue));
setOpen(false);
+2
View File
@@ -94,6 +94,7 @@ export default function DialogProvider({ children }: Props) {
className={"w-full"}
size={"sm"}
tabIndex={-1}
data-cy={"confirmation.cancel"}
onClick={() => fn.current && fn.current(false)}
>
{dialogOptions.cancelText || "Cancel"}
@@ -109,6 +110,7 @@ export default function DialogProvider({ children }: Props) {
}
className={"w-full"}
size={"sm"}
data-cy={"confirmation.confirm"}
onClick={() => fn.current && fn.current(true)}
>
{dialogOptions.confirmText || "Confirm"}
+1 -3
View File
@@ -77,9 +77,7 @@ export default function PeerProvider({ children, peer }: Props) {
? loginExpiration
: peer.login_expiration_enabled,
approval_required:
approval_required != undefined
? approval_required
: peer.approval_required,
approval_required == undefined ? undefined : approval_required,
},
`/${peer.id}`,
);
+35 -15
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;
+12
View File
@@ -10,6 +10,7 @@ export interface PostureCheck {
os_version_check?: OperatingSystemVersionCheck;
geo_location_check?: GeoLocationCheck;
peer_network_range_check?: PeerNetworkRangeCheck;
process_check?: ProcessCheck;
};
policies?: Policy[];
active?: boolean;
@@ -53,6 +54,17 @@ export interface PeerNetworkRangeCheck {
action: "allow" | "deny";
}
export interface ProcessCheck {
processes: Process[];
}
export interface Process {
id: string;
linux_path?: string;
mac_path?: string;
windows_path?: string;
}
export const windowsKernelVersions: SelectOption[] = [
{ value: "5.0", label: "Windows 2000" },
{ value: "5.1", label: "Windows XP" },
@@ -239,12 +239,6 @@ export function AccessControlModalContent({
const portAndDirectionDisabled = protocol == "icmp" || protocol == "all";
const buttonDisabled = useMemo(() => {
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
if (name.length == 0) return true;
if (direction != "bi" && ports.length == 0) return true;
}, [sourceGroups, destinationGroups, direction, ports, name]);
const [postureChecks, setPostureChecks] = useState<PostureCheck[]>([]);
const postureChecksLoaded = useRef(false);
@@ -268,6 +262,16 @@ export function AccessControlModalContent({
}
}, [initialPostureChecks]);
const continuePostureChecksDisabled = useMemo(() => {
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
if (direction != "bi" && ports.length == 0) return true;
}, [sourceGroups, destinationGroups, direction, ports]);
const submitDisabled = useMemo(() => {
if (name.length == 0) return true;
if (continuePostureChecksDisabled) return true;
}, [name, continuePostureChecksDisabled]);
return (
<ModalContent maxWidthClass={"max-w-2xl"}>
<ModalHeader
@@ -283,14 +287,17 @@ export function AccessControlModalContent({
color={"netbird"}
/>
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)} value={tab}>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"policy"}>
<ArrowRightLeft size={16} />
Policy
</TabsTrigger>
<PostureCheckTabTrigger />
<TabsTrigger value={"general"}>
<PostureCheckTabTrigger disabled={continuePostureChecksDisabled} />
<TabsTrigger
value={"general"}
disabled={continuePostureChecksDisabled}
>
<Text
size={16}
className={
@@ -456,24 +463,74 @@ export function AccessControlModalContent({
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
{!policy ? (
<>
{tab == "policy" && (
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
)}
<Button
variant={"primary"}
disabled={buttonDisabled}
onClick={submit}
>
{policy ? (
<>Save Changes</>
) : (
<>
<PlusCircle size={16} />
Add Policy
</>
)}
</Button>
{tab == "posture_checks" && (
<Button variant={"secondary"} onClick={() => setTab("policy")}>
Back
</Button>
)}
{tab == "policy" && (
<Button
variant={"primary"}
onClick={() => setTab("posture_checks")}
disabled={continuePostureChecksDisabled}
>
Continue
</Button>
)}
{tab == "posture_checks" && (
<Button
variant={"primary"}
onClick={() => setTab("general")}
disabled={continuePostureChecksDisabled}
>
Continue
</Button>
)}
{tab == "general" && (
<>
<Button
variant={"secondary"}
onClick={() => setTab("posture_checks")}
>
Back
</Button>
<Button
variant={"primary"}
disabled={submitDisabled}
onClick={submit}
>
<PlusCircle size={16} />
Add Policy
</Button>
</>
)}
</>
) : (
<>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={submitDisabled}
onClick={submit}
>
Save Changes
</Button>
</>
)}
</div>
</ModalFooter>
</ModalContent>
+55 -10
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;
}
+12 -1
View File
@@ -1,3 +1,4 @@
import Badge from "@components/Badge";
import Button from "@components/Button";
import {
Modal,
@@ -10,6 +11,7 @@ import ModalHeader from "@components/modal/ModalHeader";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import MultipleGroups from "@components/ui/MultipleGroups";
import { IconCirclePlus } from "@tabler/icons-react";
import { FolderGit2 } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
@@ -27,6 +29,7 @@ type Props = {
label?: string;
description?: string;
peer?: Peer;
showAddGroupButton?: boolean;
};
export default function GroupsRow({
@@ -37,6 +40,7 @@ export default function GroupsRow({
label = "Assigned Groups",
description = "Use groups to control what this peer can access",
peer,
showAddGroupButton = false,
}: Props) {
const { groups: allGroups } = useGroups();
const { isUser } = useLoggedInUser();
@@ -59,7 +63,14 @@ export default function GroupsRow({
setModal && !isUser && setModal(true);
}}
>
<MultipleGroups groups={foundGroups} label={label} />
{foundGroups?.length == 0 && showAddGroupButton ? (
<Badge variant={"gray"} useHover={true}>
<IconCirclePlus size={14} />
Add Groups
</Badge>
) : (
<MultipleGroups groups={foundGroups} label={label} />
)}
</ModalTrigger>
<EditGroupsModal
groups={foundGroups}
+85 -33
View File
@@ -238,27 +238,22 @@ export function NameserverModalContent({
return "";
}, [name]);
const hasAnyError = useMemo(() => {
return (
const canContinueToDomains = useMemo(() => {
return !(
hasNSErrors ||
nsError ||
domainError ||
nameservers.length == 0 ||
hasDomainErrors ||
groups.length == 0 ||
nameLengthError !== "" ||
name == ""
groups.length == 0
);
}, [
nsError,
domainError,
nameservers,
groups,
hasNSErrors,
hasDomainErrors,
nameLengthError,
name,
]);
}, [hasNSErrors, nsError, nameservers.length, groups.length]);
const canContinueToGeneral = useMemo(() => {
return !(!canContinueToDomains || domainError || hasDomainErrors);
}, [canContinueToDomains, domainError, hasDomainErrors]);
const canSubmit = useMemo(() => {
return !(!canContinueToGeneral || nameLengthError !== "" || name == "");
}, [canContinueToGeneral, nameLengthError, name]);
return (
<ModalContent maxWidthClass={"max-w-xl"}>
@@ -269,7 +264,7 @@ export function NameserverModalContent({
color={"netbird"}
/>
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)} value={tab}>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"nameserver"}>
<ServerIcon
@@ -280,7 +275,7 @@ export function NameserverModalContent({
/>
Nameserver
</TabsTrigger>
<TabsTrigger value={"domains"}>
<TabsTrigger value={"domains"} disabled={!canContinueToDomains}>
<GlobeIcon
size={16}
className={
@@ -289,7 +284,7 @@ export function NameserverModalContent({
/>
Domains
</TabsTrigger>
<TabsTrigger value={"general"}>
<TabsTrigger value={"general"} disabled={!canContinueToGeneral}>
<Text
size={16}
className={
@@ -473,20 +468,77 @@ export function NameserverModalContent({
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
{!isUpdate ? (
<>
{tab == "nameserver" && (
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
)}
<Button variant={"primary"} disabled={hasAnyError} onClick={submit}>
{isUpdate ? (
<>Save Changes</>
) : (
<>
<PlusCircle size={16} />
Add Nameserver
</>
)}
</Button>
{tab == "domains" && (
<Button
variant={"secondary"}
onClick={() => setTab("nameserver")}
>
Back
</Button>
)}
{tab == "nameserver" && (
<Button
variant={"primary"}
onClick={() => setTab("domains")}
disabled={!canContinueToDomains}
>
Continue
</Button>
)}
{tab == "domains" && (
<Button
variant={"primary"}
onClick={() => setTab("general")}
disabled={!canContinueToGeneral}
>
Continue
</Button>
)}
{tab == "general" && (
<>
<Button
variant={"secondary"}
onClick={() => setTab("domains")}
>
Back
</Button>
<Button
variant={"primary"}
disabled={!canSubmit}
onClick={submit}
>
<PlusCircle size={16} />
Add Nameserver
</Button>
</>
)}
</>
) : (
<>
<ModalClose asChild={true}>
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={!canSubmit}
onClick={submit}
>
Save Changes
</Button>
</>
)}
</div>
</ModalFooter>
</ModalContent>
@@ -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>
</>
+1 -2
View File
@@ -13,8 +13,7 @@ export const useHasExitNodes = (peer?: Peer) => {
);
return peer
? routes?.some(
(route) =>
route?.peer === peer.id && route?.network.includes("0.0.0.0"),
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
) || false
: false;
};
Loaded 20 of 37 files, more files were not shown because too many files have changed in this diff. Show more