Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f943bb7d4 | ||
|
|
96b939e6cc | ||
|
|
5e13548b81 | ||
|
|
2272a1d2a4 | ||
|
|
fc3da50346 |
@@ -57,6 +57,8 @@ import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
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";
|
||||
@@ -127,6 +129,7 @@ function PeerOverview() {
|
||||
};
|
||||
|
||||
const { isUser } = useLoggedInUser();
|
||||
const hasExitNodes = useHasExitNodes(peer);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -342,7 +345,8 @@ function PeerOverview() {
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div>
|
||||
<div className={"gap-4 flex"}>
|
||||
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
|
||||
<AddRouteDropdownButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
@@ -22,16 +22,21 @@ export default function SetupKeys() {
|
||||
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
|
||||
const { groups } = useGroups();
|
||||
|
||||
const setupKeysWithGroups = setupKeys?.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups.map((group) => {
|
||||
return groups.find((g) => g.id === group) || undefined;
|
||||
}) as Group[] | undefined,
|
||||
};
|
||||
});
|
||||
const setupKeysWithGroups = useMemo(() => {
|
||||
if (!setupKeys) return [];
|
||||
return setupKeys?.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups
|
||||
?.map((group) => {
|
||||
return groups.find((g) => g.id === group) || undefined;
|
||||
})
|
||||
.filter((group) => group !== undefined) as Group[],
|
||||
};
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
queryParams?: string;
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
useRedirect("/peers");
|
||||
return <FullScreenLoading />;
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
|
||||
"netbird-query-params",
|
||||
"",
|
||||
);
|
||||
const [queryParams, setQueryParams] = useState("");
|
||||
|
||||
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(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import deIcon from "@/assets/countries/de.svg";
|
||||
|
||||
export const CountryDERounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={deIcon}
|
||||
alt={"de"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import euIcon from "@/assets/countries/eu.svg";
|
||||
|
||||
export const CountryEURounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={euIcon}
|
||||
alt={"eu"}
|
||||
fill={true}
|
||||
className={"object-cover object-center shrink-0"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import jpIcon from "@/assets/countries/jp.svg";
|
||||
|
||||
export const CountryJPRounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={jpIcon}
|
||||
alt={"eu"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import usIcon from "@/assets/countries/us.svg";
|
||||
|
||||
export const CountryUSRounded = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-5 h-5 overflow-hidden rounded-full relative shadow-2xl border border-nb-gray-600 flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={usIcon}
|
||||
alt={"us"}
|
||||
fill={true}
|
||||
className={"object-cover object-center"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" viewBox="0 0 5 3">
|
||||
<desc>Flag of Germany</desc>
|
||||
<rect id="black_stripe" width="5" height="3" y="0" x="0" fill="#000"/>
|
||||
<rect id="red_stripe" width="5" height="2" y="1" x="0" fill="#D00"/>
|
||||
<rect id="gold_stripe" width="5" height="1" y="2" x="0" fill="#FFCE00"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 493 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 810 540"><defs><g id="d"><g id="b"><path id="a" d="M0 0v1h.5z" transform="rotate(18 3.157 -.5)"/><use xlink:href="#a" transform="scale(-1 1)"/></g><g id="c"><use xlink:href="#b" transform="rotate(72)"/><use xlink:href="#b" transform="rotate(144)"/></g><use xlink:href="#c" transform="scale(-1 1)"/></g></defs><path fill="#039" d="M0 0h810v540H0z"/><g fill="#fc0" transform="matrix(30 0 0 30 405 270)"><use xlink:href="#d" y="-6"/><use xlink:href="#d" y="6"/><g id="e"><use xlink:href="#d" x="-6"/><use xlink:href="#d" transform="rotate(-144 -2.344 -2.11)"/><use xlink:href="#d" transform="rotate(144 -2.11 -2.344)"/><use xlink:href="#d" transform="rotate(72 -4.663 -2.076)"/><use xlink:href="#d" transform="rotate(72 -5.076 .534)"/></g><use xlink:href="#e" transform="scale(-1 1)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 888 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
|
||||
<rect fill="#fff" height="600" width="900"/>
|
||||
<circle fill="#bc002d" cx="450" cy="300" r="180"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 166 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 741 B |
BIN
src/assets/fonts/Inter.ttf
Normal file
BIN
src/assets/fonts/Inter.ttf
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@ import { useOidc, useOidcUser } from "@axa-fr/react-oidc";
|
||||
import Button from "@components/Button";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import loadConfig from "@utils/config";
|
||||
import { ArrowRightIcon, LogOut } from "lucide-react";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -55,7 +55,7 @@ export const OIDCError = () => {
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
className={"mt-5"}
|
||||
onClick={() => login("/", { client_id: config.clientId })}
|
||||
onClick={() => logout("/", { client_id: config.clientId })}
|
||||
>
|
||||
Continue
|
||||
<ArrowRightIcon size={16} />
|
||||
@@ -83,7 +83,6 @@ export const OIDCError = () => {
|
||||
onClick={() => logout("/", { client_id: config.clientId })}
|
||||
>
|
||||
Logout
|
||||
<LogOut size={16} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OidcConfiguration,
|
||||
} from "@axa-fr/react-oidc/dist/vanilla/oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import loadConfig, { buildExtras } from "@utils/config";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -43,6 +44,19 @@ export default function OIDCProvider({ children }: Props) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const path = usePathname();
|
||||
const params = useSearchParams()?.toString();
|
||||
const [, setQueryParams] = useLocalStorage("netbird-query-params", params);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
params?.includes("tab") ||
|
||||
params?.includes("search") ||
|
||||
params?.includes("id")
|
||||
) {
|
||||
setQueryParams(params);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const withCustomHistory = () => {
|
||||
return {
|
||||
|
||||
@@ -12,14 +12,14 @@ export default function HelpText({
|
||||
className,
|
||||
}: Props) {
|
||||
return (
|
||||
<p
|
||||
<span
|
||||
className={cn(
|
||||
"text-[.8rem] dark:text-nb-gray-300",
|
||||
"text-[.8rem] dark:text-nb-gray-300 block font-light tracking-wide",
|
||||
margin && "mb-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import type { ErrorResponse } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import classNames from "classnames";
|
||||
@@ -88,7 +89,7 @@ export default function Notification<T>({
|
||||
{loading ? (
|
||||
<Loader2 size={14} className={"animate-spin"} />
|
||||
) : error ? (
|
||||
<XIcon size={14} />
|
||||
<IconCircleX size={24} />
|
||||
) : (
|
||||
icon || <CheckIcon size={14} />
|
||||
)}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function PeerSelector({
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative items-center group",
|
||||
"min-h-[46px] w-full relative items-center group",
|
||||
"border border-neutral-200 dark:border-nb-gray-700 justify-between py-2 px-3",
|
||||
"rounded-md bg-white text-sm dark:bg-nb-gray-900/40 flex dark:text-neutral-400/70 text-neutral-500 cursor-pointer enabled:hover:dark:bg-nb-gray-900/50",
|
||||
"disabled:opacity-40 disabled:cursor-default",
|
||||
|
||||
@@ -15,6 +15,7 @@ const iconVariant = cva(
|
||||
green: "bg-green-950 border-green-500 text-green-500",
|
||||
purple: "bg-purple-950 border-purple-500 text-purple-500",
|
||||
indigo: "bg-indigo-950 border-indigo-500 text-indigo-500",
|
||||
yellow: "bg-yellow-950 border-yellow-400 text-yellow-400",
|
||||
},
|
||||
size: {
|
||||
small: "w-8 h-8",
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Props extends IconVariant {
|
||||
description: string | React.ReactNode;
|
||||
className?: string;
|
||||
margin?: string;
|
||||
truncate?: boolean;
|
||||
}
|
||||
export default function ModalHeader({
|
||||
icon,
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from "@components/Button";
|
||||
import { CommandItem } from "@components/Command";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { SelectDropdownSearchInput } from "@components/select/SelectDropdownSearchInput";
|
||||
@@ -31,6 +32,7 @@ interface SelectDropdownProps {
|
||||
popoverWidth?: "auto" | number;
|
||||
options: SelectOption[];
|
||||
showSearch?: boolean;
|
||||
showValues?: boolean;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
isLoading?: boolean;
|
||||
@@ -43,6 +45,7 @@ export function SelectDropdown({
|
||||
popoverWidth = "auto",
|
||||
options,
|
||||
showSearch = false,
|
||||
showValues = false,
|
||||
placeholder = "Select...",
|
||||
searchPlaceholder = "Search...",
|
||||
isLoading = false,
|
||||
@@ -186,6 +189,7 @@ export function SelectDropdown({
|
||||
option={option}
|
||||
toggle={toggle}
|
||||
key={option.value}
|
||||
showValue={showValues}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -201,9 +205,11 @@ export function SelectDropdown({
|
||||
const SelectDropdownItem = ({
|
||||
option,
|
||||
toggle,
|
||||
showValue = false,
|
||||
}: {
|
||||
option: SelectOption;
|
||||
toggle: (value: string) => void;
|
||||
showValue?: boolean;
|
||||
}) => {
|
||||
const value = option.value || "" + option.label || "";
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
@@ -233,6 +239,13 @@ const SelectDropdownItem = ({
|
||||
<span className={"text-nb-gray-200"}>{option.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
{showValue && (
|
||||
<div className={"flex items-center gap-2.5 p-1"}>
|
||||
<Paragraph className={cn("text-sm text-right")}>
|
||||
{option.value}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</CommandItem>
|
||||
) : (
|
||||
<div className={"h-[35px] py-1 px-2"}></div>
|
||||
|
||||
@@ -55,11 +55,15 @@ declare module "@tanstack/table-core" {
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
const val = row.getValue(columnId);
|
||||
if (!val) return false;
|
||||
if (typeof val !== "string") return false;
|
||||
const lowerCaseValue = removeAllSpaces(trim(value.toLowerCase()));
|
||||
return val.toLowerCase().includes(lowerCaseValue);
|
||||
try {
|
||||
const val = row.getValue(columnId);
|
||||
if (!val) return false;
|
||||
if (typeof val !== "string") return false;
|
||||
const lowerCaseValue = removeAllSpaces(trim(value.toLowerCase()));
|
||||
return val.toLowerCase().includes(lowerCaseValue);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const exactMatch: FilterFn<any> = (row, columnId, value) => {
|
||||
|
||||
@@ -21,14 +21,14 @@ export default function GroupBadge({
|
||||
}: Props) {
|
||||
return (
|
||||
<Badge
|
||||
key={group.name}
|
||||
key={group.id}
|
||||
useHover={true}
|
||||
variant={"gray-ghost"}
|
||||
className={cn("transition-all group whitespace-nowrap", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<FolderGit2 size={12} className={"shrink-0"} />
|
||||
<TextWithTooltip text={group.name} maxChars={20} />
|
||||
<TextWithTooltip text={group?.name || ""} maxChars={20} />
|
||||
{children}
|
||||
{showX && (
|
||||
<XIcon
|
||||
|
||||
@@ -24,14 +24,21 @@ export default function TextWithTooltip({
|
||||
<FullTooltip
|
||||
disabled={charCount <= maxChars || hideTooltip}
|
||||
interactive={false}
|
||||
className={"truncate w-full"}
|
||||
className={"truncate w-full min-w-0"}
|
||||
content={
|
||||
<div className={"max-w-xs break-all whitespace-normal"}>{text}</div>
|
||||
<div className={"max-w-xs break-all whitespace-normal text-xs"}>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className={cn(className, "truncate")}>
|
||||
{charCount > maxChars ? text && `${text.slice(0, maxChars)}...` : text}
|
||||
</span>
|
||||
<div
|
||||
className={"w-full min-w-0 inline-block"}
|
||||
style={{
|
||||
maxWidth: `${maxChars - 2}ch`,
|
||||
}}
|
||||
>
|
||||
<div className={cn(className, "truncate")}>{text}</div>
|
||||
</div>
|
||||
</FullTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const RoutesContext = React.createContext(
|
||||
);
|
||||
|
||||
export default function RoutesProvider({ children }: Props) {
|
||||
const routeRequest = useApiCall<Route>("/routes");
|
||||
const routeRequest = useApiCall<Route>("/routes", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const updateRoute = async (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { cn } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { Viewport } from "next/dist/lib/metadata/types/extra-types";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import OIDCProvider from "@/auth/OIDCProvider";
|
||||
@@ -16,7 +16,10 @@ import ErrorBoundaryProvider from "@/contexts/ErrorBoundary";
|
||||
import { GlobalThemeProvider } from "@/contexts/GlobalThemeProvider";
|
||||
import { NavigationEvents } from "@/contexts/NavigationEvents";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const inter = localFont({
|
||||
src: "../assets/fonts/Inter.ttf",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// Extend dayjs with relativeTime plugin
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -477,15 +477,46 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
// TODO add activity texts
|
||||
// rule.add
|
||||
// rule.update
|
||||
// rule.delete
|
||||
// setupkey.update
|
||||
// setupkey.overuse
|
||||
// group.update
|
||||
// group.delete
|
||||
// user.peer.login
|
||||
if (event.activity_code == "transferred.owner.role")
|
||||
return <div className={"inline"}>Owner role was transferred</div>;
|
||||
|
||||
/**
|
||||
* EDR
|
||||
*/
|
||||
if (event.activity_code == "integrated-validator.api.created")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m?.platform}</Value> integration created
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.api.updated")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m?.platform}</Value> integration updated
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.api.deleted")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
<Value>{m?.platform}</Value> integration deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.host-check.approved")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer approved by <Value>{m?.platform}</Value> integration
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "integrated-validator.host-check.denied")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer rejected by <Value>{m?.platform}</Value> integration
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2.5 items-center"}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ArrowLeftRight,
|
||||
Blocks,
|
||||
Cog,
|
||||
CreditCardIcon,
|
||||
FolderGit2,
|
||||
Globe,
|
||||
HelpCircleIcon,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
LogIn,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
RefreshCcw,
|
||||
Server,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
@@ -71,10 +73,22 @@ export default function ActivityTypeIcon({
|
||||
return <User size={size} className={cn(DEFAULT_CLASSES, className)} />;
|
||||
} else if (code.startsWith("service")) {
|
||||
return <Cog size={size} className={cn(DEFAULT_CLASSES, className)} />;
|
||||
} else if (code.startsWith("billing")) {
|
||||
return (
|
||||
<CreditCardIcon size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else if (code.startsWith("integrated")) {
|
||||
return (
|
||||
<ShieldCheck size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else if (code.startsWith("posture")) {
|
||||
return (
|
||||
<ShieldCheck size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else if (code.startsWith("transferred")) {
|
||||
return (
|
||||
<RefreshCcw size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<HelpCircleIcon size={size} className={cn(DEFAULT_CLASSES, className)} />
|
||||
|
||||
@@ -10,6 +10,7 @@ type Props = {
|
||||
leftSection?: React.ReactNode;
|
||||
text?: string | React.ReactNode;
|
||||
className?: string;
|
||||
additionalInfo?: React.ReactNode;
|
||||
};
|
||||
export default function ActiveInactiveRow({
|
||||
active,
|
||||
@@ -18,11 +19,12 @@ export default function ActiveInactiveRow({
|
||||
leftSection,
|
||||
inactiveDot = "gray",
|
||||
className,
|
||||
additionalInfo,
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-3 dark:text-neutral-300 text-neutral-500 min-w-[250px] max-w-[250px]",
|
||||
"gap-3 dark:text-neutral-300 text-neutral-500 min-w-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -34,9 +36,12 @@ export default function ActiveInactiveRow({
|
||||
inactiveDot={inactiveDot}
|
||||
className={"mt-1 shrink-0"}
|
||||
/>
|
||||
<div className={"flex flex-col"}>
|
||||
<div className={" font-medium"}>
|
||||
<div className={"flex flex-col min-w-0"}>
|
||||
<div
|
||||
className={"font-medium flex gap-2 items-center justify-center"}
|
||||
>
|
||||
<TextWithTooltip text={text as string} maxChars={25} />
|
||||
{additionalInfo}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ export function NameserverModalContent({
|
||||
preset,
|
||||
cell,
|
||||
}: ModalProps) {
|
||||
const nsRequest = useApiCall<NameserverGroup>("/dns/nameservers");
|
||||
const nsRequest = useApiCall<NameserverGroup>("/dns/nameservers", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const isUpdate = useMemo(() => {
|
||||
@@ -233,24 +233,31 @@ export function NameserverModalContent({
|
||||
return domains.some((d) => d.name === "");
|
||||
}, [domains]);
|
||||
|
||||
const nameLengthError = useMemo(() => {
|
||||
if (name.length > 40) return "Name should be less than 40 characters";
|
||||
return "";
|
||||
}, [name]);
|
||||
|
||||
const hasAnyError = useMemo(() => {
|
||||
return (
|
||||
hasNSErrors ||
|
||||
nsError ||
|
||||
domainError ||
|
||||
name == "" ||
|
||||
nameservers.length == 0 ||
|
||||
hasDomainErrors ||
|
||||
groups.length == 0
|
||||
groups.length == 0 ||
|
||||
nameLengthError !== "" ||
|
||||
name == ""
|
||||
);
|
||||
}, [
|
||||
nsError,
|
||||
domainError,
|
||||
name,
|
||||
nameservers,
|
||||
groups,
|
||||
hasNSErrors,
|
||||
hasDomainErrors,
|
||||
nameLengthError,
|
||||
name,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -427,6 +434,7 @@ export function NameserverModalContent({
|
||||
<Input
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
error={nameLengthError}
|
||||
placeholder={"e.g., Public DNS"}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -516,7 +524,7 @@ function NameserverInput({
|
||||
const validCIDR = cidr.isValidAddress(ip);
|
||||
if (!validCIDR) {
|
||||
onError && onError(true);
|
||||
return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
|
||||
return "Please enter a valid IP, e.g., 192.168.1.0";
|
||||
}
|
||||
onError && onError(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -532,7 +540,7 @@ function NameserverInput({
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={"IP"}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
placeholder={"e.g., 172.16.0.0"}
|
||||
maxWidthClass={"w-full"}
|
||||
value={ip}
|
||||
className={"font-mono !text-[13px]"}
|
||||
|
||||
46
src/modules/exit-node/AddExitNodeButton.tsx
Normal file
46
src/modules/exit-node/AddExitNodeButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Button from "@components/Button";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { ExitNodeHelpTooltip } from "@/modules/exit-node/ExitNodeHelpTooltip";
|
||||
import { RouteModalContent } from "@/modules/routes/RouteModal";
|
||||
|
||||
type Props = {
|
||||
peer?: Peer;
|
||||
firstTime?: boolean;
|
||||
};
|
||||
export const AddExitNodeButton = ({ peer, firstTime = false }: Props) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExitNodeHelpTooltip>
|
||||
<Button variant={"secondary"} onClick={() => setModal(true)}>
|
||||
{!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}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
40
src/modules/exit-node/ExitNodeDropdownButton.tsx
Normal file
40
src/modules/exit-node/ExitNodeDropdownButton.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { RouteModalContent } from "@/modules/routes/RouteModal";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
|
||||
export const ExitNodeDropdownButton = ({ peer }: Props) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
const isLinux = getOperatingSystem(peer.os) === OperatingSystem.LINUX;
|
||||
|
||||
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>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<Modal open={modal} onOpenChange={setModal}>
|
||||
{modal && (
|
||||
<RouteModalContent
|
||||
onSuccess={() => setModal(false)}
|
||||
peer={peer}
|
||||
exitNode={true}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
47
src/modules/exit-node/ExitNodeHelpTooltip.tsx
Normal file
47
src/modules/exit-node/ExitNodeHelpTooltip.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
hoverButton?: boolean;
|
||||
};
|
||||
export const ExitNodeHelpTooltip = ({
|
||||
children,
|
||||
hoverButton = false,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FullTooltip
|
||||
hoverButton={hoverButton}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
An exit node is a network route that routes all your internet
|
||||
traffic through one of your peers.
|
||||
<div className={"mt-2"}>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/configuring-default-routes-for-internet-traffic"
|
||||
}
|
||||
target={"_blank"}
|
||||
className={"mr-1"}
|
||||
>
|
||||
Exit Nodes
|
||||
<ExternalLinkIcon size={10} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
src/modules/exit-node/ExitNodePeerIndicator.tsx
Normal file
25
src/modules/exit-node/ExitNodePeerIndicator.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
export const ExitNodePeerIndicator = ({ peer }: Props) => {
|
||||
const hasExitNode = useHasExitNodes(peer);
|
||||
|
||||
return hasExitNode ? (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
This peer is an exit node. Traffic from the configured distribution
|
||||
groups will be routed through this peer.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconDirectionSign size={15} className={"text-yellow-400 shrink-0"} />
|
||||
</FullTooltip>
|
||||
) : null;
|
||||
};
|
||||
20
src/modules/exit-node/useHasExitNodes.tsx
Normal file
20
src/modules/exit-node/useHasExitNodes.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
|
||||
export const useHasExitNodes = (peer?: Peer) => {
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
const { data: routes } = useFetchApi<Route[]>(
|
||||
`/routes`,
|
||||
false,
|
||||
true,
|
||||
isOwnerOrAdmin,
|
||||
);
|
||||
return peer
|
||||
? routes?.some(
|
||||
(route) =>
|
||||
route?.peer === peer.id && route?.network.includes("0.0.0.0"),
|
||||
) || false
|
||||
: false;
|
||||
};
|
||||
@@ -142,7 +142,8 @@ export function GroupSelector({
|
||||
<div className={""}>
|
||||
<div className={"grid grid-cols-1 gap-1"}>
|
||||
{orderBy(groups, "name")?.map((item) => {
|
||||
const value = item.name;
|
||||
const value = item?.name || "";
|
||||
if (value === "") return null;
|
||||
const isSelected =
|
||||
values.find((c) => c == value) != undefined;
|
||||
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
import { ExitNodeHelpTooltip } from "@/modules/exit-node/ExitNodeHelpTooltip";
|
||||
|
||||
type Props = {
|
||||
route: Route;
|
||||
};
|
||||
export default function PeerRouteNetworkCell({ route }: Props) {
|
||||
return (
|
||||
const isExitNode = route?.network === "0.0.0.0/0";
|
||||
|
||||
return isExitNode ? (
|
||||
<ExitNodeHelpTooltip>
|
||||
<div className={"flex gap-2 items-center dark:text-nb-gray-300 group"}>
|
||||
<IconDirectionSign size={16} className={"text-yellow-400"} />
|
||||
Exit Node{" "}
|
||||
<InfoIcon
|
||||
size={14}
|
||||
className={
|
||||
"text-nb-gray-500 group-hover:text-nb-gray-400 transition-all"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ExitNodeHelpTooltip>
|
||||
) : (
|
||||
<div className={"font-mono dark:text-nb-gray-300 flex max-w-[10px]"}>
|
||||
{route.network}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { usePeer } from "@/contexts/PeerProvider";
|
||||
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
||||
|
||||
export default function PeerActionCell() {
|
||||
const { peer, deletePeer, update, openSSHDialog } = usePeer();
|
||||
@@ -125,6 +126,9 @@ export default function PeerActionCell() {
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<ExitNodeDropdownButton peer={peer} />
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={deletePeer} variant={"danger"}>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import { useLoggedInUser, useUsers } from "@/contexts/UsersProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import ActiveInactiveRow from "@/modules/common-table-rows/ActiveInactiveRow";
|
||||
import { ExitNodePeerIndicator } from "@/modules/exit-node/ExitNodePeerIndicator";
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
@@ -11,22 +12,33 @@ type Props = {
|
||||
export default function PeerNameCell({ peer }: Props) {
|
||||
const { users } = useUsers();
|
||||
const router = useRouter();
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
|
||||
const userOfPeer = useMemo(() => {
|
||||
return users?.find((user) => user.id === peer.user_id);
|
||||
}, [users, peer.user_id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex items-center min-w-[250px] max-w-[250px] gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
data-testid="peer-name-cell"
|
||||
onClick={() => router.push("/peer?id=" + peer.id)}
|
||||
>
|
||||
<ActiveInactiveRow active={peer.connected} text={peer.name}>
|
||||
<div className={"text-nb-gray-400 font-light"}>{userOfPeer?.email}</div>
|
||||
</ActiveInactiveRow>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex items-center max-w-[300px] gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
data-testid="peer-name-cell"
|
||||
onClick={() => router.push("/peer?id=" + peer.id)}
|
||||
>
|
||||
<ActiveInactiveRow
|
||||
active={peer.connected}
|
||||
text={peer.name}
|
||||
additionalInfo={
|
||||
isOwnerOrAdmin && <ExitNodePeerIndicator peer={peer} />
|
||||
}
|
||||
>
|
||||
<div className={"text-nb-gray-400 font-light truncate"}>
|
||||
{userOfPeer?.email}
|
||||
</div>
|
||||
</ActiveInactiveRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "group_name_strings",
|
||||
accessorFn: (peer) => peer.groups?.map((g) => g.name).join(", "),
|
||||
accessorFn: (peer) => peer.groups?.map((g) => g?.name || "").join(", "),
|
||||
sortingFn: "text",
|
||||
},
|
||||
{
|
||||
accessorKey: "group_names",
|
||||
accessorFn: (peer) => peer.groups?.map((g) => g.name),
|
||||
accessorFn: (peer) => peer.groups?.map((g) => g?.name || ""),
|
||||
sortingFn: "text",
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
|
||||
@@ -2,8 +2,7 @@ import { cn } from "@utils/helpers";
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import { FaWindows } from "react-icons/fa6";
|
||||
import { CountryDERounded } from "@/assets/countries/CountryDERounded";
|
||||
import { CountryUSRounded } from "@/assets/countries/CountryUSRounded";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import AppleLogo from "@/assets/os-icons/apple.svg";
|
||||
|
||||
@@ -24,7 +23,7 @@ export const PostureCheckIcons = () => {
|
||||
"h-6 w-6 overflow-hidden rounded-full flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<CountryDERounded />
|
||||
<RoundedFlag country="de" />
|
||||
</div>
|
||||
</Circle>
|
||||
<Circle className={"z-[3]"}>
|
||||
@@ -36,7 +35,7 @@ export const PostureCheckIcons = () => {
|
||||
"h-6 w-6 overflow-hidden rounded-full flex items-center justify-center"
|
||||
}
|
||||
>
|
||||
<CountryUSRounded />
|
||||
<RoundedFlag country="us" />
|
||||
</div>
|
||||
</Circle>
|
||||
<Circle className={"z-[1] top-2 "}>
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { ExitNodeHelpTooltip } from "@/modules/exit-node/ExitNodeHelpTooltip";
|
||||
|
||||
type Props = {
|
||||
network: string;
|
||||
};
|
||||
export default function GroupedRouteNetworkRangeCell({ network }: Props) {
|
||||
return (
|
||||
const isExitNode = network === "0.0.0.0/0";
|
||||
|
||||
return isExitNode ? (
|
||||
<ExitNodeHelpTooltip>
|
||||
<div className={"flex gap-2 items-center dark:text-nb-gray-300 group"}>
|
||||
<IconDirectionSign size={16} className={"text-yellow-400"} />
|
||||
Exit Node{" "}
|
||||
<InfoIcon
|
||||
size={14}
|
||||
className={
|
||||
"text-nb-gray-500 group-hover:text-nb-gray-400 transition-all"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ExitNodeHelpTooltip>
|
||||
) : (
|
||||
<div className={"font-mono dark:text-nb-gray-300 flex max-w-[10px]"}>
|
||||
{network}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import GroupRouteProvider from "@/contexts/GroupRouteProvider";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { GroupedRoute, Route } from "@/interfaces/Route";
|
||||
import { AddExitNodeButton } from "@/modules/exit-node/AddExitNodeButton";
|
||||
import GroupedRouteActionCell from "@/modules/route-group/GroupedRouteActionCell";
|
||||
import GroupedRouteHighAvailabilityCell from "@/modules/route-group/GroupedRouteHighAvailabilityCell";
|
||||
import GroupedRouteNameCell from "@/modules/route-group/GroupedRouteNameCell";
|
||||
@@ -157,12 +158,15 @@ export default function NetworkRoutesTable({
|
||||
"It looks like you don't have any routes. Access LANs and VPC by adding a network route."
|
||||
}
|
||||
button={
|
||||
<RouteModal>
|
||||
<Button variant={"primary"} className={""}>
|
||||
<PlusCircle size={16} />
|
||||
Add Route
|
||||
</Button>
|
||||
</RouteModal>
|
||||
<div className={"gap-x-4 flex items-center justify-center"}>
|
||||
<AddExitNodeButton />
|
||||
<RouteModal>
|
||||
<Button variant={"primary"} className={""}>
|
||||
<PlusCircle size={16} />
|
||||
Add Route
|
||||
</Button>
|
||||
</RouteModal>
|
||||
</div>
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
@@ -183,12 +187,15 @@ export default function NetworkRoutesTable({
|
||||
rightSide={() => (
|
||||
<>
|
||||
{routes && routes?.length > 0 && (
|
||||
<RouteModal>
|
||||
<Button variant={"primary"} className={"ml-auto"}>
|
||||
<PlusCircle size={16} />
|
||||
Add Route
|
||||
</Button>
|
||||
</RouteModal>
|
||||
<div className={"gap-x-4 ml-auto flex"}>
|
||||
<AddExitNodeButton />
|
||||
<RouteModal>
|
||||
<Button variant={"primary"} className={""}>
|
||||
<PlusCircle size={16} />
|
||||
Add Route
|
||||
</Button>
|
||||
</RouteModal>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PeerSelector } from "@components/PeerSelector";
|
||||
import { SegmentedTabs } from "@components/SegmentedTabs";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { Textarea } from "@components/Textarea";
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import cidr from "ip-cidr";
|
||||
import { uniqBy } from "lodash";
|
||||
@@ -63,19 +64,36 @@ export default function RouteModal({ children }: Props) {
|
||||
type ModalProps = {
|
||||
onSuccess?: (route: Route) => void;
|
||||
peer?: Peer;
|
||||
exitNode?: boolean;
|
||||
isFirstExitNode?: boolean;
|
||||
};
|
||||
|
||||
export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
export function RouteModalContent({
|
||||
onSuccess,
|
||||
peer,
|
||||
exitNode,
|
||||
isFirstExitNode = false,
|
||||
}: ModalProps) {
|
||||
const { createRoute } = useRoutes();
|
||||
const [tab, setTab] = useState("network");
|
||||
|
||||
// General
|
||||
const [networkIdentifier, setNetworkIdentifier] = useState("");
|
||||
/**
|
||||
* Network Identifier, Description & Network Range
|
||||
*/
|
||||
const [networkIdentifier, setNetworkIdentifier] = useState(
|
||||
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("");
|
||||
const [networkRange, setNetworkRange] = useState(exitNode ? "0.0.0.0/0" : "");
|
||||
const [routingPeer, setRoutingPeer] = useState<Peer | undefined>(peer);
|
||||
|
||||
const [
|
||||
routingPeerGroups,
|
||||
setRoutingPeerGroups,
|
||||
@@ -84,29 +102,23 @@ export function RouteModalContent({ onSuccess, peer }: 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();
|
||||
@@ -147,36 +159,91 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
);
|
||||
};
|
||||
|
||||
// Is button disabled
|
||||
const isDisabled = useMemo(() => {
|
||||
return (
|
||||
networkIdentifier == "" ||
|
||||
/**
|
||||
* 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 isNetworkEntered = useMemo(() => {
|
||||
return !(
|
||||
(cidrError && cidrError.length > 1) ||
|
||||
(peerTab === "peer-group" && routingPeerGroups.length == 0) ||
|
||||
(peerTab === "routing-peer" && !routingPeer) ||
|
||||
groups.length == 0
|
||||
groups.length == 0 ||
|
||||
networkRange == ""
|
||||
);
|
||||
}, [
|
||||
networkIdentifier,
|
||||
cidrError,
|
||||
peerTab,
|
||||
routingPeerGroups.length,
|
||||
routingPeer,
|
||||
groups,
|
||||
networkRange,
|
||||
]);
|
||||
|
||||
const [tab, setTab] = useState("network");
|
||||
const networkIdentifierError = useMemo(() => {
|
||||
return (networkIdentifier?.length || 0) > 40
|
||||
? "Network Identifier must be less than 40 characters"
|
||||
: "";
|
||||
}, [networkIdentifier]);
|
||||
|
||||
const metricError = useMemo(() => {
|
||||
return parseInt(metric) < 1 || parseInt(metric) > 9999
|
||||
? "Metric must be between 1 and 9999"
|
||||
: "";
|
||||
}, [metric]);
|
||||
|
||||
const isNameEntered = useMemo(() => {
|
||||
return networkIdentifier != "" && networkIdentifierError == "";
|
||||
}, [networkIdentifier, networkIdentifierError]);
|
||||
|
||||
const canCreateOrSave = useMemo(() => {
|
||||
return isNetworkEntered && isNameEntered && metricError == "";
|
||||
}, [isNetworkEntered, isNameEntered, metricError]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-xl"}>
|
||||
<ModalHeader
|
||||
icon={<NetworkRoutesIcon className={"fill-netbird"} />}
|
||||
title={"Create New Route"}
|
||||
description={"Access LANs and VPC by adding a network route."}
|
||||
color={"netbird"}
|
||||
icon={
|
||||
exitNode ? (
|
||||
<IconDirectionSign size={20} />
|
||||
) : (
|
||||
<NetworkRoutesIcon className={"fill-netbird"} />
|
||||
)
|
||||
}
|
||||
title={
|
||||
exitNode
|
||||
? isFirstExitNode
|
||||
? "Setup Exit Node"
|
||||
: "Add Exit Node"
|
||||
: "Create New Route"
|
||||
}
|
||||
truncate={!!peer}
|
||||
description={
|
||||
exitNode
|
||||
? peer
|
||||
? `Route all traffic through the peer '${peer.name}'`
|
||||
: "Route all internet traffic through a peer"
|
||||
: "Access LANs and VPC by adding a network route."
|
||||
}
|
||||
color={exitNode ? "yellow" : "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={"network"}
|
||||
@@ -192,6 +259,7 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={"general"}
|
||||
disabled={!isNetworkEntered}
|
||||
onClick={() => nameRef.current?.focus()}
|
||||
>
|
||||
<Text
|
||||
@@ -202,7 +270,10 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
/>
|
||||
Name & Description
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"settings"}>
|
||||
<TabsTrigger
|
||||
value={"settings"}
|
||||
disabled={!isNetworkEntered || !isNameEntered}
|
||||
>
|
||||
<Settings2
|
||||
size={16}
|
||||
className={
|
||||
@@ -212,6 +283,78 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
Additional Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value={"network"} className={"pb-8"}>
|
||||
<div className={"px-8 flex-col flex gap-6"}>
|
||||
<div className={cn(exitNode && "hidden")}>
|
||||
<Label>Network Range</Label>
|
||||
<HelpText>Add a private IP address range</HelpText>
|
||||
<Input
|
||||
ref={networkRangeRef}
|
||||
customPrefix={<NetworkIcon size={16} />}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
value={networkRange}
|
||||
className={"font-mono !text-[13px]"}
|
||||
error={cidrError}
|
||||
onChange={(e) => setNetworkRange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{exitNode && peer ? (
|
||||
<></>
|
||||
) : (
|
||||
<SegmentedTabs value={peerTab} onChange={setPeerTab}>
|
||||
<SegmentedTabs.List>
|
||||
<SegmentedTabs.Trigger value={"routing-peer"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Routing Peer
|
||||
</SegmentedTabs.Trigger>
|
||||
|
||||
<SegmentedTabs.Trigger value={"peer-group"} disabled={!!peer}>
|
||||
<FolderGit2 size={16} />
|
||||
Peer Group
|
||||
</SegmentedTabs.Trigger>
|
||||
</SegmentedTabs.List>
|
||||
<SegmentedTabs.Content value={"routing-peer"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a single peer as a routing peer for the
|
||||
{exitNode ? " exit node." : " Network CIDR."}
|
||||
</HelpText>
|
||||
<PeerSelector
|
||||
onChange={setRoutingPeer}
|
||||
value={routingPeer}
|
||||
disabled={!!peer}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
<SegmentedTabs.Content value={"peer-group"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a peer group with Linux machines to be used as
|
||||
{exitNode ? " exit nodes." : "routing peers."}
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
max={1}
|
||||
onChange={setRoutingPeerGroups}
|
||||
values={routingPeerGroups}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
</SegmentedTabs>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label>Distribution Groups</Label>
|
||||
<HelpText>
|
||||
{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>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value={"general"} className={"px-8 pb-6"}>
|
||||
<div className={"flex flex-col gap-6"}>
|
||||
<div>
|
||||
@@ -220,6 +363,7 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
Add a unique network identifier that is assigned to each device.
|
||||
</HelpText>
|
||||
<Input
|
||||
error={networkIdentifierError}
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
ref={nameRef}
|
||||
@@ -244,69 +388,6 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value={"network"} className={"pb-8"}>
|
||||
<div className={"px-8 flex-col flex gap-6"}>
|
||||
<div>
|
||||
<Label>Network Range</Label>
|
||||
<HelpText>Add a private IP address range</HelpText>
|
||||
<Input
|
||||
ref={networkRangeRef}
|
||||
customPrefix={<NetworkIcon size={16} />}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
value={networkRange}
|
||||
className={"font-mono !text-[13px]"}
|
||||
error={cidrError}
|
||||
onChange={(e) => setNetworkRange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<SegmentedTabs value={peerTab} onChange={setPeerTab}>
|
||||
<SegmentedTabs.List>
|
||||
<SegmentedTabs.Trigger value={"routing-peer"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Routing Peer
|
||||
</SegmentedTabs.Trigger>
|
||||
|
||||
<SegmentedTabs.Trigger value={"peer-group"} disabled={!!peer}>
|
||||
<FolderGit2 size={16} />
|
||||
Peer Group
|
||||
</SegmentedTabs.Trigger>
|
||||
</SegmentedTabs.List>
|
||||
<SegmentedTabs.Content value={"routing-peer"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a single peer as a routing peer for the Network CIDR.
|
||||
</HelpText>
|
||||
<PeerSelector
|
||||
onChange={setRoutingPeer}
|
||||
value={routingPeer}
|
||||
disabled={!!peer}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
<SegmentedTabs.Content value={"peer-group"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign peer group with Linux machines to be used as routing
|
||||
peers.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
max={1}
|
||||
onChange={setRoutingPeerGroups}
|
||||
values={routingPeerGroups}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
</SegmentedTabs>
|
||||
<div>
|
||||
<Label>Distribution Groups</Label>
|
||||
<HelpText>
|
||||
Advertise this route to peers that belong to the following
|
||||
groups
|
||||
</HelpText>
|
||||
<PeerGroupSelector onChange={setGroups} values={groups} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value={"settings"} className={"pb-4"}>
|
||||
<div className={"px-8 flex flex-col gap-6"}>
|
||||
<FancyToggleSwitch
|
||||
@@ -320,19 +401,22 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
}
|
||||
helpText={"Use this switch to enable or disable the route."}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={masquerade}
|
||||
onChange={setMasquerade}
|
||||
label={
|
||||
<>
|
||||
<VenetianMask size={15} />
|
||||
Masquerade
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Allow access to your private networks without configuring routes on your local routers or other devices."
|
||||
}
|
||||
/>
|
||||
{!exitNode && (
|
||||
<FancyToggleSwitch
|
||||
value={masquerade}
|
||||
onChange={setMasquerade}
|
||||
label={
|
||||
<>
|
||||
<VenetianMask size={15} />
|
||||
Masquerade
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Allow access to your private networks without configuring routes on your local routers or other devices."
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={cn("flex justify-between")}>
|
||||
<div>
|
||||
<Label>Metrics</Label>
|
||||
@@ -346,6 +430,8 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
max={9999}
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
value={metric}
|
||||
error={metricError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
onChange={(e) => setMetric(e.target.value)}
|
||||
customPrefix={
|
||||
@@ -366,28 +452,64 @@ export function RouteModalContent({ onSuccess, peer }: ModalProps) {
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
exitNode
|
||||
? "https://docs.netbird.io/how-to/configuring-default-routes-for-internet-traffic"
|
||||
: "https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Network Routes
|
||||
{exitNode ? "Exit Nodes" : "Network Routes"}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
{tab == "network" && (
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={isDisabled}
|
||||
onClick={createRouteHandler}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Route
|
||||
</Button>
|
||||
{tab == "general" && (
|
||||
<Button variant={"secondary"} onClick={() => setTab("network")}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "settings" && (
|
||||
<Button variant={"secondary"} onClick={() => setTab("general")}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "network" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("general")}
|
||||
disabled={!isNetworkEntered}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
{tab == "general" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("settings")}
|
||||
disabled={!isNameEntered || !isNetworkEntered}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
{tab == "settings" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canCreateOrSave}
|
||||
onClick={createRouteHandler}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{exitNode ? "Add Exit Node" : "Add Route"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -197,14 +197,21 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
.filter((p) => p != undefined) as string[];
|
||||
}, [groupedRoute]);
|
||||
|
||||
const metricError = useMemo(() => {
|
||||
return parseInt(metric.toString()) < 1 || parseInt(metric.toString()) > 9999
|
||||
? "Metric must be between 1 and 9999"
|
||||
: "";
|
||||
}, [metric]);
|
||||
|
||||
// Is button disabled
|
||||
const isDisabled = useMemo(() => {
|
||||
return (
|
||||
(peerTab === "peer-group" && routingPeerGroups.length == 0) ||
|
||||
(peerTab === "routing-peer" && !routingPeer) ||
|
||||
groups.length == 0
|
||||
groups.length == 0 ||
|
||||
metricError !== ""
|
||||
);
|
||||
}, [peerTab, routingPeerGroups.length, routingPeer, groups]);
|
||||
}, [peerTab, routingPeerGroups.length, routingPeer, groups, metricError]);
|
||||
|
||||
const [tab, setTab] = useState(
|
||||
cell && cell == "metric" ? "settings" : "network",
|
||||
@@ -352,6 +359,8 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
max={9999}
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
value={metric}
|
||||
error={metricError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
onChange={(e) => setMetric(e.target.value)}
|
||||
customPrefix={
|
||||
|
||||
@@ -17,11 +17,11 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
|
||||
|
||||
const handleRevoke = async () => {
|
||||
notify({
|
||||
title: "Setup Key: " + setupKey.name,
|
||||
title: setupKey?.name || "Setup Key",
|
||||
description: "Setup key was successfully revoked",
|
||||
promise: deleteRequest
|
||||
.put({
|
||||
name: setupKey.name,
|
||||
name: setupKey?.name || "Setup Key",
|
||||
type: setupKey.type,
|
||||
expires_in: setupKey.expires_in,
|
||||
revoked: true,
|
||||
@@ -39,7 +39,7 @@ export default function SetupKeyActionCell({ setupKey }: Props) {
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Revoke '${setupKey.name}'?`,
|
||||
title: `Revoke '${setupKey?.name || "Setup Key"}'?`,
|
||||
description:
|
||||
"Are you sure you want to revoke the setup key? This action cannot be undone.",
|
||||
confirmText: "Revoke",
|
||||
|
||||
@@ -17,15 +17,15 @@ export default function SetupKeyGroupsCell({ setupKey }: Props) {
|
||||
const groups = await Promise.all(promises);
|
||||
|
||||
notify({
|
||||
title: setupKey.name,
|
||||
title: setupKey?.name || "Setup Key",
|
||||
description: "Groups of the setup key were successfully saved",
|
||||
promise: request
|
||||
.put({
|
||||
name: setupKey.name,
|
||||
name: setupKey?.name || "Setup Key",
|
||||
type: setupKey.type,
|
||||
expires_in: setupKey.expires_in,
|
||||
revoked: setupKey.revoked,
|
||||
auto_groups: groups.map((group) => group.id),
|
||||
auto_groups: groups?.map((group) => group.id) || [],
|
||||
usage_limit: setupKey.usage_limit,
|
||||
ephemeral: setupKey.ephemeral,
|
||||
})
|
||||
|
||||
@@ -126,7 +126,7 @@ type ModalProps = {
|
||||
};
|
||||
|
||||
export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys");
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
@@ -143,10 +143,18 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
return reusable ? "Unlimited" : "1";
|
||||
}, [reusable]);
|
||||
|
||||
const expiresInError = useMemo(() => {
|
||||
const expires = parseInt(expiresIn);
|
||||
if (expires < 1 || expires > 365) {
|
||||
return "Days should be between 1 and 365";
|
||||
}
|
||||
return "";
|
||||
}, [expiresIn]);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
const trimmedName = trim(name);
|
||||
return trimmedName.length === 0;
|
||||
}, [name]);
|
||||
return trimmedName.length === 0 || expiresInError.length > 0;
|
||||
}, [name, expiresInError]);
|
||||
|
||||
const submit = () => {
|
||||
if (!selectedGroups) return;
|
||||
@@ -245,6 +253,8 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
min={1}
|
||||
max={365}
|
||||
value={expiresIn}
|
||||
error={expiresInError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
onChange={(e) => setExpiresIn(e.target.value)}
|
||||
customPrefix={
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SetupKeyModal open={open} setOpen={setOpen} />
|
||||
{open && <SetupKeyModal open={open} setOpen={setOpen} />}
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Setup Keys"}
|
||||
|
||||
@@ -39,7 +39,10 @@ export const SetupKeysTableColumns: ColumnDef<SetupKey>[] = [
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => (
|
||||
<SetupKeyNameCell valid={row.original.valid} name={row.original.name} />
|
||||
<SetupKeyNameCell
|
||||
valid={row.original.valid}
|
||||
name={row.original?.name || ""}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -66,7 +69,7 @@ export const SetupKeysTableColumns: ColumnDef<SetupKey>[] = [
|
||||
{
|
||||
id: "group_strings",
|
||||
accessorKey: "group_strings",
|
||||
accessorFn: (s) => s.groups?.map((g) => g.name).join(", "),
|
||||
accessorFn: (s) => s.groups?.map((g) => g?.name || "").join(", "),
|
||||
},
|
||||
{
|
||||
accessorKey: "last_used",
|
||||
|
||||
@@ -93,6 +93,7 @@ export default function useFetchApi<T>(
|
||||
url: string,
|
||||
ignoreError = false,
|
||||
revalidate = true,
|
||||
allowFetch = true,
|
||||
) {
|
||||
const { fetch } = useNetBirdFetch(ignoreError);
|
||||
const handleErrors = useApiErrorHandling(ignoreError);
|
||||
@@ -100,6 +101,7 @@ export default function useFetchApi<T>(
|
||||
const { data, error, isLoading, isValidating, mutate } = useSWR(
|
||||
url,
|
||||
async (url) => {
|
||||
if (!allowFetch) return;
|
||||
return apiRequest<T>(fetch, "GET", url).catch((err) =>
|
||||
handleErrors(err as ErrorResponse),
|
||||
);
|
||||
@@ -167,13 +169,13 @@ export function useApiErrorHandling(ignoreError = false) {
|
||||
return login(currentPath);
|
||||
}
|
||||
if (err.code == 401 && err.message == "token invalid") {
|
||||
return setError(err);
|
||||
setError(err);
|
||||
}
|
||||
if (err.code == 500 && err.message == "internal server error") {
|
||||
return setError(err);
|
||||
setError(err);
|
||||
}
|
||||
if (err.code > 400 && err.code <= 500) {
|
||||
return setError(err);
|
||||
setError(err);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
|
||||
Reference in New Issue
Block a user