mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e553f3c83 | ||
|
|
79164e9dd5 | ||
|
|
5caeab118b |
No files matched your search
@@ -5,6 +5,9 @@ const nextConfig = {
|
||||
unoptimized: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
APP_ENV: process.env.APP_ENV || "production",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 />;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
Reference in new issue
Block a user