mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92212063fa | ||
|
|
fd8d96019f | ||
|
|
8684c32055 |
No files matched your search
@@ -5,9 +5,6 @@ const nextConfig = {
|
||||
unoptimized: true,
|
||||
},
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
APP_ENV: process.env.APP_ENV || "production",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
@@ -23,7 +23,6 @@ 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";
|
||||
@@ -67,11 +66,8 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||
export default function PeerPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
const peerId = queryParameter.get("id");
|
||||
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
|
||||
|
||||
useRedirect("/peers", false, !peerId);
|
||||
|
||||
return peer && !isLoading ? (
|
||||
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
|
||||
return peer ? (
|
||||
<PeerProvider peer={peer}>
|
||||
<PeerOverview />
|
||||
</PeerProvider>
|
||||
|
||||
@@ -10,7 +10,6 @@ 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";
|
||||
@@ -43,8 +42,6 @@ export default function UserPage() {
|
||||
return users?.find((u) => u.id === userId);
|
||||
}, [users, userId]);
|
||||
|
||||
useRedirect("/team/users", false, !userId);
|
||||
|
||||
return !isLoading && user ? (
|
||||
<UserOverview user={user} />
|
||||
) : (
|
||||
|
||||
+6
-32
@@ -1,40 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
queryParams?: string;
|
||||
};
|
||||
export default function NotFound() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
|
||||
"netbird-query-params",
|
||||
"",
|
||||
);
|
||||
const [queryParams, setQueryParams] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
setQueryParams(tempQueryParams);
|
||||
setTempQueryParams("");
|
||||
setMounted(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
router.push("/peers");
|
||||
});
|
||||
|
||||
return mounted ? (
|
||||
<Redirect
|
||||
url={window?.location?.pathname || "/"}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
}
|
||||
@@ -16,8 +16,6 @@ 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,16 +5,14 @@ import React, { forwardRef } from "react";
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ButtonGroup({ children, disabled, className }: Props) {
|
||||
function ButtonGroup({ children, disabled }: 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}
|
||||
@@ -23,10 +21,7 @@ function ButtonGroup({ children, disabled, className }: Props) {
|
||||
}
|
||||
|
||||
const ButtonGroupButton = forwardRef(
|
||||
(
|
||||
{ className, ...props }: ButtonProps,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
({ ...props }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -36,7 +31,6 @@ 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,7 +198,6 @@ 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,19 +21,12 @@ function SegmentedTabs({ value, onChange, children }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function List({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
function List({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<TabsList
|
||||
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,
|
||||
)}
|
||||
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"
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</TabsList>
|
||||
|
||||
@@ -75,10 +75,7 @@ const ModalContent = React.forwardRef<
|
||||
<>
|
||||
{children}
|
||||
{showClose && (
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -28,10 +28,9 @@ 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 shrink-0"} />
|
||||
<RowsIcon size={15} className={"text-nb-gray-300"} />
|
||||
<div>
|
||||
<span className={"text-white"}>
|
||||
{table.getState().pagination.pageSize}
|
||||
@@ -48,7 +47,6 @@ 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,7 +94,6 @@ 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"}
|
||||
@@ -110,7 +109,6 @@ 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,7 +77,9 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
? loginExpiration
|
||||
: peer.login_expiration_enabled,
|
||||
approval_required:
|
||||
approval_required == undefined ? undefined : approval_required,
|
||||
approval_required != undefined
|
||||
? approval_required
|
||||
: peer.approval_required,
|
||||
},
|
||||
`/${peer.id}`,
|
||||
);
|
||||
|
||||
+15
-35
@@ -1,9 +1,8 @@
|
||||
import loadConfig from "@utils/config";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
export const useRedirect = (
|
||||
url: string,
|
||||
replace: boolean = false,
|
||||
@@ -11,43 +10,24 @@ export const useRedirect = (
|
||||
) => {
|
||||
const router = useRouter();
|
||||
const currentPath = usePathname();
|
||||
const callBackUrls = useRef([config.redirectURI, config.silentRedirectURI]);
|
||||
const isRedirecting = useRef(false);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const callBackUrls = [config.redirectURI, config.silentRedirectURI];
|
||||
|
||||
useEffect(() => {
|
||||
// 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;
|
||||
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
|
||||
|
||||
const performRedirect = () => {
|
||||
if (!isRedirecting.current) {
|
||||
isRedirecting.current = true;
|
||||
router.refresh();
|
||||
if (replace) {
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
isRedirecting.current = false;
|
||||
}
|
||||
};
|
||||
const redirect = replace ? router.replace : router.push; // Replace the current history or add a new one
|
||||
|
||||
performRedirect();
|
||||
router.refresh();
|
||||
redirect(url);
|
||||
|
||||
// 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);
|
||||
// Timer in case the user has his browser tab open but not focused
|
||||
const interval = setInterval(() => {
|
||||
router.refresh();
|
||||
redirect(url);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [replace, router, url, enable, currentPath]);
|
||||
return () => clearInterval(interval);
|
||||
}, [replace, router, url, enable]);
|
||||
};
|
||||
|
||||
export default useRedirect;
|
||||
@@ -10,7 +10,6 @@ export interface PostureCheck {
|
||||
os_version_check?: OperatingSystemVersionCheck;
|
||||
geo_location_check?: GeoLocationCheck;
|
||||
peer_network_range_check?: PeerNetworkRangeCheck;
|
||||
process_check?: ProcessCheck;
|
||||
};
|
||||
policies?: Policy[];
|
||||
active?: boolean;
|
||||
@@ -54,17 +53,6 @@ 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" },
|
||||
|
||||
@@ -3,11 +3,7 @@ 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 = {
|
||||
@@ -58,8 +54,7 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "setupkey.peer.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -149,24 +144,21 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "user.peer.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||
NetBird IP <Value>{m.ip}</Value> was deleted
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.peer.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.peer.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||
NetBird IP <Value>{m.ip}</Value> was updated
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was updated
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -260,15 +252,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
|
||||
NetBird IP <Value>{m.peer_ip}</Value>
|
||||
Group <Value>{m.group}</Value> was removed from the peer with the 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 NetBird IP{" "}
|
||||
Group <Value>{m.group}</Value> was added to the peer with the ip{" "}
|
||||
<Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
@@ -311,7 +303,7 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "peer.rename")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the NetBird IP <Value>{m.ip}</Value> was renamed to{" "}
|
||||
Peer with the ip <Value>{m.ip}</Value> was renamed to{" "}
|
||||
<Value>{m.name}</Value>
|
||||
</div>
|
||||
);
|
||||
@@ -319,7 +311,7 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "peer.approve")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the NetBird IP <Value>{m.ip}</Value> was approved
|
||||
Peer with the ip <Value>{m.ip}</Value> was approved
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -567,7 +559,7 @@ function Value({
|
||||
return children ? (
|
||||
<span
|
||||
className={cn(
|
||||
"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]",
|
||||
"text-nb-gray-200 inline font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -575,40 +567,3 @@ 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,4 +1,3 @@
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
Modal,
|
||||
@@ -11,7 +10,6 @@ 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";
|
||||
@@ -29,7 +27,6 @@ type Props = {
|
||||
label?: string;
|
||||
description?: string;
|
||||
peer?: Peer;
|
||||
showAddGroupButton?: boolean;
|
||||
};
|
||||
|
||||
export default function GroupsRow({
|
||||
@@ -40,7 +37,6 @@ 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();
|
||||
@@ -63,14 +59,7 @@ export default function GroupsRow({
|
||||
setModal && !isUser && setModal(true);
|
||||
}}
|
||||
>
|
||||
{foundGroups?.length == 0 && showAddGroupButton ? (
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<IconCirclePlus size={14} />
|
||||
Add Groups
|
||||
</Badge>
|
||||
) : (
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
)}
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
</ModalTrigger>
|
||||
<EditGroupsModal
|
||||
groups={foundGroups}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
|
||||
import { 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 = {
|
||||
@@ -17,41 +15,24 @@ 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"}>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
<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 && (
|
||||
<RoutesProvider>
|
||||
<RouteModalContent
|
||||
onSuccess={() => setModal(false)}
|
||||
peer={peer}
|
||||
exitNode={true}
|
||||
/>
|
||||
</RoutesProvider>
|
||||
<RouteModalContent
|
||||
onSuccess={() => setModal(false)}
|
||||
peer={peer}
|
||||
exitNode={true}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -13,7 +13,8 @@ export const useHasExitNodes = (peer?: Peer) => {
|
||||
);
|
||||
return peer
|
||||
? routes?.some(
|
||||
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
|
||||
(route) =>
|
||||
route?.peer === peer.id && route?.network.includes("0.0.0.0"),
|
||||
) || false
|
||||
: false;
|
||||
};
|
||||
@@ -1,310 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { ModalClose, ModalFooter } from "@components/modal/Modal";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { isEmpty, uniqueId } from "lodash";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
MinusCircleIcon,
|
||||
PlusCircle,
|
||||
ServerCogIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import AppleIcon from "@/assets/icons/AppleIcon";
|
||||
import WindowsIcon from "@/assets/icons/WindowsIcon";
|
||||
import { Process, ProcessCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
|
||||
type Props = {
|
||||
value?: ProcessCheck;
|
||||
onChange: (value: ProcessCheck | undefined) => void;
|
||||
};
|
||||
|
||||
export const PostureCheckProcess = ({ value, onChange }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PostureCheckCard
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
key={open ? 1 : 0}
|
||||
active={value?.processes && value?.processes?.length > 0}
|
||||
title={"Process"}
|
||||
description={
|
||||
"Restrict access in your network based on running processes of a peer."
|
||||
}
|
||||
icon={<ServerCogIcon size={18} />}
|
||||
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
|
||||
modalWidthClass={"max-w-xl"}
|
||||
onReset={() => onChange(undefined)}
|
||||
>
|
||||
<CheckContent
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PostureCheckCard>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange }: Props) => {
|
||||
const [processes, setProcesses] = useState<Process[]>(
|
||||
value?.processes
|
||||
? value.processes.map((p) => {
|
||||
return {
|
||||
id: uniqueId("process"),
|
||||
linux_path: p?.linux_path || "",
|
||||
mac_path: p?.mac_path || "",
|
||||
windows_path: p?.windows_path || "",
|
||||
};
|
||||
})
|
||||
: [
|
||||
{
|
||||
id: uniqueId("process"),
|
||||
linux_path: "",
|
||||
mac_path: "",
|
||||
windows_path: "",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const handleProcessChange = (
|
||||
id: string,
|
||||
linux_path: string,
|
||||
mac_path: string,
|
||||
windows_path: string,
|
||||
) => {
|
||||
const newProcesses = processes.map((p) =>
|
||||
p.id === id ? { ...p, linux_path, mac_path, windows_path } : p,
|
||||
);
|
||||
setProcesses(newProcesses);
|
||||
};
|
||||
|
||||
const removeProcess = (id: string) => {
|
||||
const newProcesses = processes.filter((p) => p.id !== id);
|
||||
setProcesses(newProcesses);
|
||||
};
|
||||
|
||||
const addProcess = () => {
|
||||
setProcesses([
|
||||
...processes,
|
||||
{
|
||||
id: uniqueId("process"),
|
||||
linux_path: "",
|
||||
mac_path: "",
|
||||
windows_path: "",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const pathErrors = useMemo(() => {
|
||||
if (processes && processes.length > 0) {
|
||||
return processes.map((p) => {
|
||||
return {
|
||||
id: p.id,
|
||||
errorMacPath: p?.mac_path
|
||||
? validator.isValidUnixFilePath(p?.mac_path || "")
|
||||
? ""
|
||||
: "Please enter a valid macOS file path"
|
||||
: "",
|
||||
errorLinuxPath: p?.linux_path
|
||||
? validator.isValidUnixFilePath(p?.linux_path || "")
|
||||
? ""
|
||||
: "Please enter a valid Unix file path"
|
||||
: "",
|
||||
errorWindowsPath: p?.windows_path
|
||||
? validator.isValidWindowsFilePath(p?.windows_path || "")
|
||||
? ""
|
||||
: "Please enter a valid Windows file path"
|
||||
: "",
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [processes]);
|
||||
|
||||
const hasErrorsOrIsEmpty = useMemo(() => {
|
||||
if (processes.length === 0) return true;
|
||||
const hasOnlyEmptyPaths = processes.some(
|
||||
(p) => p.linux_path === "" && p.mac_path === "" && p.windows_path === "",
|
||||
);
|
||||
const hasPathErrors = pathErrors.some(
|
||||
(e) =>
|
||||
e.errorLinuxPath !== "" ||
|
||||
e.errorMacPath !== "" ||
|
||||
e.errorWindowsPath !== "",
|
||||
);
|
||||
return hasOnlyEmptyPaths || hasPathErrors;
|
||||
}, [processes, pathErrors]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"flex flex-col px-8 gap-2 pb-6"}>
|
||||
<div className={"flex justify-between items-start gap-10 mt-2"}>
|
||||
<div>
|
||||
<Label>Processes</Label>
|
||||
<HelpText className={""}>
|
||||
Add the path of an executable file of the process. You can define
|
||||
a path for Linux, macOS and Windows. Peers will only be allowed to
|
||||
connect if the process is running on their system.
|
||||
</HelpText>
|
||||
</div>
|
||||
</div>
|
||||
{processes.length > 0 && (
|
||||
<div className={"mb-2 flex flex-col gap-4 w-full "}>
|
||||
{processes.map((p) => {
|
||||
return (
|
||||
<div key={p.id} className={"flex gap-2 items-center"}>
|
||||
<div className={"w-full flex flex-col gap-1.5"}>
|
||||
<Input
|
||||
customPrefix={<TerminalIcon size={16} />}
|
||||
placeholder={"/usr/local/bin/netbird"}
|
||||
value={p.linux_path}
|
||||
error={
|
||||
pathErrors.find((e) => e.id === p.id)?.errorLinuxPath
|
||||
}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top-right"}
|
||||
className={"w-full"}
|
||||
onChange={(e) =>
|
||||
handleProcessChange(
|
||||
p.id,
|
||||
e.target.value,
|
||||
p?.mac_path || "",
|
||||
p?.windows_path || "",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
customPrefix={
|
||||
<AppleIcon
|
||||
size={16}
|
||||
className={cn(
|
||||
pathErrors.find((e) => e.id === p.id)
|
||||
?.errorMacPath && "fill-red-500",
|
||||
)}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
"/Applications/NetBird.app/Contents/MacOS/netbird"
|
||||
}
|
||||
value={p.mac_path}
|
||||
error={
|
||||
pathErrors.find((e) => e.id === p.id)?.errorMacPath
|
||||
}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top-right"}
|
||||
className={"w-full"}
|
||||
onChange={(e) =>
|
||||
handleProcessChange(
|
||||
p.id,
|
||||
p?.linux_path || "",
|
||||
e.target.value,
|
||||
p?.windows_path || "",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
customPrefix={
|
||||
<WindowsIcon
|
||||
size={16}
|
||||
className={cn(
|
||||
pathErrors.find((e) => e.id === p.id)
|
||||
?.errorWindowsPath && "fill-red-500",
|
||||
)}
|
||||
/>
|
||||
}
|
||||
placeholder={`C:\\ProgramData\\NetBird\\netbird.exe`}
|
||||
value={p.windows_path}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top-right"}
|
||||
error={
|
||||
pathErrors.find((e) => e.id === p.id)?.errorWindowsPath
|
||||
}
|
||||
className={"w-full"}
|
||||
onChange={(e) =>
|
||||
handleProcessChange(
|
||||
p.id,
|
||||
p?.linux_path || "",
|
||||
p?.mac_path || "",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={() => removeProcess(p.id)}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant={"dotted"}
|
||||
size={"sm"}
|
||||
onClick={addProcess}
|
||||
className={"mt-1"}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Process
|
||||
</Button>
|
||||
</div>
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-posture-checks#process-check"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Process Check
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={hasErrorsOrIsEmpty}
|
||||
onClick={() => {
|
||||
if (isEmpty(processes)) {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
onChange({
|
||||
processes: processes.filter(
|
||||
(p) =>
|
||||
p.linux_path !== "" ||
|
||||
p.mac_path !== "" ||
|
||||
p.windows_path !== "",
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { tryGetProcessNameFromPath } from "@utils/helpers";
|
||||
import { TerminalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import AppleIcon from "@/assets/icons/AppleIcon";
|
||||
import WindowsIcon from "@/assets/icons/WindowsIcon";
|
||||
import { ProcessCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
type Props = {
|
||||
check?: ProcessCheck;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const ProcessTooltip = ({ check, children }: Props) => {
|
||||
return check ? (
|
||||
<FullTooltip
|
||||
className={"w-full min-w-0"}
|
||||
interactive={true}
|
||||
contentClassName={"p-0"}
|
||||
content={
|
||||
<div
|
||||
className={
|
||||
"text-neutral-300 text-sm max-w-xs flex flex-col gap-1 min-w-0"
|
||||
}
|
||||
>
|
||||
<div className={"px-4 pt-3"}>
|
||||
<span>
|
||||
<span className={"text-green-500 font-semibold"}>Allow only</span>{" "}
|
||||
peers which are running the following processes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
className={
|
||||
"max-h-[275px] overflow-y-auto flex flex-col px-4 min-w-0"
|
||||
}
|
||||
>
|
||||
<div className={"flex flex-col gap-3 mt-1 text-xs mb-3.5 min-w-0"}>
|
||||
{check.processes.map((p, index) => {
|
||||
return (
|
||||
<div className={"flex-col flex gap-1 min-w-0"} key={index}>
|
||||
{p?.linux_path && (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
className={"justify-start font-medium text-xs min-w-0"}
|
||||
>
|
||||
<span className={"mr-1.5"}>
|
||||
<TerminalIcon size={12} />
|
||||
</span>
|
||||
<span
|
||||
className={"truncate inline-block "}
|
||||
title={p?.linux_path}
|
||||
>
|
||||
{tryGetProcessNameFromPath(p?.linux_path) ||
|
||||
"Unknown path"}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{p?.mac_path && (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
className={"justify-start font-medium text-xs min-w-0"}
|
||||
>
|
||||
<span className={"mr-1.5"}>
|
||||
<AppleIcon size={12} />
|
||||
</span>
|
||||
<span
|
||||
className={"truncate inline-block "}
|
||||
title={p?.mac_path}
|
||||
>
|
||||
{tryGetProcessNameFromPath(p?.mac_path) ||
|
||||
"Unknown path"}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{p?.windows_path && (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
className={"justify-start font-medium text-xs min-w-0"}
|
||||
>
|
||||
<span className={"mr-1.5"}>
|
||||
<WindowsIcon size={12} />
|
||||
</span>
|
||||
<span
|
||||
className={"truncate inline-block"}
|
||||
title={p?.windows_path}
|
||||
>
|
||||
{tryGetProcessNameFromPath(p?.windows_path) ||
|
||||
"Unknown path"}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
Loaded 20 of 34 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user