Compare commits

...

5 Commits

Author SHA1 Message Date
Eduard Gert
8e7bcc0c22 Extend posture checks with peer network range check (#344)
Some checks failed
build and push / build_n_push (push) Has been cancelled
add support to peer network checks
2024-02-27 16:15:47 +01:00
Eduard Gert
02a0b71e46 Fix setup key modal closing on first time creation (#342) 2024-02-26 18:02:56 +01:00
Eduard Gert
a8b66d935f Show loading indicator for peer detail view as groups are loading (#343) 2024-02-26 18:02:28 +01:00
Eduard Gert
f74f9cf812 Add region and public ip to peer table and detailed peer view (#340)
* Fix group badge icon size

* Fix copy icon size

* Add region information to peer table and single peer view

* Push to docker

* Change login expired icon size

* Fix country flag in single peer view

* Change country flag size in peer table

* Disable revalidation for countries

* Fix icon size on peer detail view

* Rollback workflow

* Revert login expiration

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2024-02-23 15:52:33 +01:00
Maycon Santos
7578595f05 Update posture checks documentation links (#339)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2024-02-22 21:46:55 +01:00
32 changed files with 773 additions and 173 deletions

View File

@@ -2,6 +2,7 @@ name: build and push
on:
push:
branches:
- "feature/**"
- main
tags:
- "**"

View File

@@ -26,23 +26,28 @@ import TextWithTooltip from "@components/ui/TextWithTooltip";
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import dayjs from "dayjs";
import { trim } from "lodash";
import { isEmpty, trim } from "lodash";
import {
Cpu,
FlagIcon,
Globe,
History,
MapPin,
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
TerminalSquare,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { toASCII } from "punycode";
import React, { useMemo, useState } from "react";
import Skeleton from "react-loading-skeleton";
import { useSWRConfig } from "swr";
import RoundedFlag from "@/assets/countries/RoundedFlag";
import CircleIcon from "@/assets/icons/CircleIcon";
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import { useCountries } from "@/contexts/CountryProvider";
import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
@@ -139,7 +144,7 @@ function PeerOverview() {
<CircleIcon
active={peer.connected}
size={12}
className={"mb-[3px]"}
className={"mb-[3px] shrink-0"}
/>
<TextWithTooltip text={name} maxChars={30} />
@@ -291,6 +296,12 @@ function PeerOverview() {
}
function PeerInformationCard({ peer }: { peer: Peer }) {
const { isLoading, getRegionByPeer } = useCountries();
const countryText = useMemo(() => {
return getRegionByPeer(peer);
}, [getRegionByPeer, peer]);
return (
<Card>
<Card.List>
@@ -304,6 +315,44 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
value={peer.ip}
/>
<Card.ListItem
label={
<>
<NetworkIcon size={16} />
Public IP-Address
</>
}
value={peer.connection_ip}
/>
<Card.ListItem
label={
<>
<FlagIcon size={16} />
Region
</>
}
tooltip={false}
value={
isEmpty(peer.country_code) ? (
"Unknown"
) : (
<>
{isLoading ? (
<Skeleton width={140} />
) : (
<div className={"flex gap-2 items-center"}>
<div className={"border-0 border-nb-gray-800 rounded-full"}>
<RoundedFlag country={peer.country_code} size={12} />
</div>
{countryText}
</div>
)}
</>
)
}
/>
<Card.ListItem
label={
<>
@@ -347,6 +396,7 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
")"
}
/>
<Card.ListItem
label={
<>

View File

@@ -48,7 +48,7 @@ export default function PostureChecksPage() {
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={"#"} target={"_blank"}>
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks"} target={"_blank"}>
Posture Checks
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -30,6 +30,7 @@ type CardListItemProps = {
value: React.ReactNode;
className?: string;
copy?: boolean;
tooltip?: boolean;
};
function CardListItem({
@@ -37,6 +38,7 @@ function CardListItem({
value,
className,
copy = false,
tooltip = true,
}: CardListItemProps) {
const [, copyToClipBoard] = useCopyToClipboard(value as string);
@@ -57,7 +59,11 @@ function CardListItem({
copy && copyToClipBoard(`${label} has been copied to clipboard.`)
}
>
<TextWithTooltip text={value as string} maxChars={40} />
{tooltip ? (
<TextWithTooltip text={value as string} maxChars={40} />
) : (
value
)}
{copy && <Copy size={13} />}
</div>
</li>

View File

@@ -28,12 +28,16 @@ export default function CopyToClipboardText({ children, message }: Props) {
{copied ? (
<CheckIcon
className={"text-nb-gray-100 opacity-0 group-hover:opacity-100"}
className={
"text-nb-gray-100 opacity-0 group-hover:opacity-100 shrink-0"
}
size={12}
/>
) : (
<CopyIcon
className={"text-nb-gray-100 opacity-0 group-hover:opacity-100"}
className={
"text-nb-gray-100 opacity-0 group-hover:opacity-100 shrink-0"
}
size={12}
/>
)}

View File

@@ -50,6 +50,7 @@ export default function FullTooltip({
className={cn(
isAction ? "cursor-pointer" : "cursor-default",
"inline-flex items-center 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",
className,
)}
>
{children}

View File

@@ -0,0 +1,36 @@
import Skeleton from "react-loading-skeleton";
export default function SkeletonPeerDetail() {
return (
<div className={"w-full mt-6 p-default"}>
<div className={"flex flex-wrap w-full justify-between max-w-6xl "}>
<Skeleton height={24} width={300} className={"rounded-md"} />
</div>
<div className={"flex flex-wrap w-full justify-between mt-4 max-w-6xl "}>
<Skeleton height={42} width={400} className={"rounded-md"} />
<div className={"flex gap-3"}>
<Skeleton height={42} width={80} className={"rounded-md"} />
<Skeleton height={42} width={120} className={"rounded-md"} />
</div>
</div>
<div
className={
"flex flex-wrap w-full justify-between mt-6 max-w-6xl gap-10"
}
>
<Skeleton
height={400}
width={"100%"}
className={"rounded-md"}
containerClassName={"flex-1 "}
/>
<Skeleton
height={300}
width={"100%"}
className={"rounded-md opacity-30"}
containerClassName={"flex-1 "}
/>
</div>
</div>
);
}

View File

@@ -2,19 +2,16 @@ import {
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import useFetchApi from "@utils/api";
import { createElement, useMemo } from "react";
import RoundedFlag from "@/assets/countries/RoundedFlag";
import { Country } from "@/interfaces/Country";
import { useCountries } from "@/contexts/CountryProvider";
type Props = {
value: string;
onChange: (value: string) => void;
};
export const CountrySelector = ({ value, onChange }: Props) => {
const { data: countries, isLoading } = useFetchApi<Country[]>(
"/locations/countries",
);
const { countries, isLoading } = useCountries();
const countryList = useMemo(() => {
return countries?.map((country) => {

View File

@@ -27,11 +27,14 @@ export default function GroupBadge({
className={cn("transition-all group whitespace-nowrap", className)}
onClick={onClick}
>
<FolderGit2 size={12} />
<FolderGit2 size={12} className={"shrink-0"} />
<TextWithTooltip text={group.name} maxChars={20} />
{children}
{showX && (
<XIcon size={12} className={"cursor-pointer group-hover:text-white"} />
<XIcon
size={12}
className={"cursor-pointer group-hover:text-white shrink-0"}
/>
)}
</Badge>
);

View File

@@ -10,7 +10,7 @@ export default function LoginExpiredBadge({ loginExpired }: Props) {
<Tooltip delayDuration={1}>
<TooltipTrigger>
<Badge variant={"red"} className={"px-3"}>
<AlertTriangle size={14} className={"mr-1"} />
<AlertTriangle size={13} className={"mr-1"} />
Login required
</Badge>
</TooltipTrigger>

View File

@@ -24,11 +24,12 @@ export default function TextWithTooltip({
<FullTooltip
disabled={charCount <= maxChars || hideTooltip}
interactive={false}
className={"truncate w-full"}
content={
<div className={"max-w-xs break-all whitespace-normal"}>{text}</div>
}
>
<span className={cn(className)}>
<span className={cn(className, "truncate")}>
{charCount > maxChars ? text && `${text.slice(0, maxChars)}...` : text}
</span>
</FullTooltip>

View File

@@ -0,0 +1,47 @@
import useFetchApi from "@utils/api";
import React, { useCallback } from "react";
import { Country } from "@/interfaces/Country";
import { Peer } from "@/interfaces/Peer";
type Props = {
children: React.ReactNode;
};
const CountryContext = React.createContext(
{} as {
countries: Country[] | undefined;
isLoading: boolean;
getRegionByPeer: (peer: Peer) => string;
},
);
export default function CountryProvider({ children }: Props) {
const { data: countries, isLoading } = useFetchApi<Country[]>(
"/locations/countries",
false,
false,
);
const getRegionByPeer = useCallback(
(peer: Peer) => {
if (!countries) return "Unknown";
const country = countries.find(
(c) => c.country_code === peer.country_code,
);
if (!country) return "Unknown";
if (!peer.city_name) return country.country_name;
return `${country.country_name}, ${peer.city_name}`;
},
[countries],
);
return (
<CountryContext.Provider value={{ countries, isLoading, getRegionByPeer }}>
{children}
</CountryContext.Provider>
);
}
export const useCountries = () => {
return React.useContext(CountryContext);
};

View File

@@ -12,11 +12,12 @@ const GroupContext = React.createContext(
refresh: () => void;
dropdownOptions: Group[];
setDropdownOptions: React.Dispatch<React.SetStateAction<Group[]>>;
isLoading: boolean;
},
);
export default function GroupsProvider({ children }: Props) {
const { data: groups, mutate } = useFetchApi<Group[]>("/groups");
const { data: groups, mutate, isLoading } = useFetchApi<Group[]>("/groups");
const [dropdownOptions, setDropdownOptions] = useState<Group[]>([]);
const refresh = () => {
@@ -25,7 +26,13 @@ export default function GroupsProvider({ children }: Props) {
return (
<GroupContext.Provider
value={{ groups, refresh, dropdownOptions, setDropdownOptions }}
value={{
groups,
refresh,
dropdownOptions,
setDropdownOptions,
isLoading,
}}
>
{children}
</GroupContext.Provider>

View File

@@ -1,4 +1,5 @@
import { notify } from "@components/Notification";
import SkeletonPeerDetail from "@components/skeletons/SkeletonPeerDetail";
import { useApiCall } from "@utils/api";
import React, { useMemo } from "react";
import { useSWRConfig } from "swr";
@@ -27,12 +28,13 @@ const PeerContext = React.createContext(
) => Promise<Peer>;
openSSHDialog: () => Promise<boolean>;
deletePeer: () => void;
isLoading: boolean;
},
);
export default function PeerProvider({ children, peer }: Props) {
const user = usePeerUser(peer);
const peerGroups = usePeerGroups(peer);
const { peerGroups, isLoading } = usePeerGroups(peer);
const peerRequest = useApiCall<Peer>("/peers");
const { confirm } = useDialog();
const { mutate } = useSWRConfig();
@@ -94,12 +96,22 @@ export default function PeerProvider({ children, peer }: Props) {
});
};
return (
return !isLoading ? (
<PeerContext.Provider
value={{ peer, peerGroups, user, update, openSSHDialog, deletePeer }}
value={{
peer,
peerGroups,
user,
update,
openSSHDialog,
deletePeer,
isLoading,
}}
>
{children}
</PeerContext.Provider>
) : (
<SkeletonPeerDetail />
);
}
@@ -108,9 +120,9 @@ export default function PeerProvider({ children, peer }: Props) {
* @param peer
*/
export const usePeerGroups = (peer?: Peer) => {
const { groups } = useGroups();
const { groups, isLoading } = useGroups();
return useMemo(() => {
const peerGroups = useMemo(() => {
if (!peer) return [];
const peerGroups = groups?.filter((group) => {
const foundGroup = group.peers?.find((p) => {
@@ -121,6 +133,8 @@ export const usePeerGroups = (peer?: Peer) => {
});
return peerGroups || [];
}, [groups, peer]);
return { peerGroups, isLoading };
};
/**

View File

@@ -20,4 +20,7 @@ export interface Peer {
login_expired: boolean;
login_expiration_enabled: boolean;
approval_required: boolean;
city_name: string;
country_code: string;
connection_ip: string;
}

View File

@@ -9,6 +9,7 @@ export interface PostureCheck {
nb_version_check?: NetBirdVersionCheck;
os_version_check?: OperatingSystemVersionCheck;
geo_location_check?: GeoLocationCheck;
peer_network_range_check?: PeerNetworkRangeCheck;
};
policies?: Policy[];
active?: boolean;
@@ -47,6 +48,11 @@ export interface GeoLocation {
city_name: string;
}
export interface PeerNetworkRangeCheck {
ranges: string[];
action: "allow" | "deny";
}
export const windowsKernelVersions: SelectOption[] = [
{ value: "5.0", label: "Windows 2000" },
{ value: "5.1", label: "Windows XP" },

View File

@@ -12,6 +12,7 @@ import React from "react";
import ApplicationProvider, {
useApplicationContext,
} from "@/contexts/ApplicationProvider";
import CountryProvider from "@/contexts/CountryProvider";
import GroupsProvider from "@/contexts/GroupsProvider";
import UsersProvider from "@/contexts/UsersProvider";
import Navigation from "@/layouts/Navigation";
@@ -26,7 +27,9 @@ export default function DashboardLayout({
<ApplicationProvider>
<UsersProvider>
<GroupsProvider>
<DashboardPageContent>{children}</DashboardPageContent>
<CountryProvider>
<DashboardPageContent>{children}</DashboardPageContent>
</CountryProvider>
</GroupsProvider>
</UsersProvider>
</ApplicationProvider>

View File

@@ -30,7 +30,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
}, [groups, initial]);
const [selectedGroups, setSelectedGroups] = useState<Group[]>(initialGroups);
const peerGroups = usePeerGroups(peer);
const { peerGroups } = usePeerGroups(peer);
const save = async () => {
return Promise.all(getAllGroupCalls()).then((groups) => {

View File

@@ -9,7 +9,7 @@ type Props = {
};
export default function usePeerRoutes({ peer }: Props) {
const { data: routes } = useFetchApi<Route[]>("/routes");
const peerGroups = usePeerGroups(peer);
const { peerGroups } = usePeerGroups(peer);
return useMemo(() => {
if (!routes) return undefined;

View File

@@ -1,43 +1,61 @@
import CopyToClipboardText from "@components/CopyToClipboardText";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import FullTooltip from "@components/FullTooltip";
import { cn } from "@utils/helpers";
import { isEmpty } from "lodash";
import { GlobeIcon } from "lucide-react";
import React from "react";
import RoundedFlag from "@/assets/countries/RoundedFlag";
import { Peer } from "@/interfaces/Peer";
import { PeerAddressTooltipContent } from "@/modules/peers/PeerAddressTooltipContent";
type Props = {
peer: Peer;
};
export default function PeerAddressCell({ peer }: Props) {
return (
<div className={"flex gap-4 items-center min-w-[320px] max-w-[320px]"}>
<FullTooltip
side={"top"}
interactive={false}
contentClassName={"p-0"}
content={<PeerAddressTooltipContent peer={peer} />}
>
<div
className={cn(
"flex items-center justify-center rounded-md h-8 w-8 shrink-0",
peer.connected ? "bg-green-600" : "bg-nb-gray-800 opacity-50",
)}
className={
"flex gap-4 items-center min-w-[320px] max-w-[320px] group/cell transition-all hover:bg-nb-gray-800/10 py-2 px-3 rounded-md cursor-default"
}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<GlobeIcon size={14} className={"shrink-0"} />
</div>
<div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light">
<CopyToClipboardText
message={"DNS label has been copied to your clipboard"}
<div
className={cn(
"flex items-center justify-center rounded-full h-8 w-8 shrink-0 bg-nb-gray-920/80 transition-all",
)}
>
<span className={"font-normal"}>
<TextWithTooltip
text={peer.dns_label}
maxChars={40}
className={"whitespace-nowrap"}
/>
</span>
</CopyToClipboardText>
<CopyToClipboardText
message={"IP address has been copied to your clipboard"}
>
<span className={"dark:text-nb-gray-400 font-mono font-thin text-xs"}>
{peer.ip}
</span>
</CopyToClipboardText>
{isEmpty(peer.country_code) ? (
<GlobeIcon size={16} className={"text-nb-gray-300"} />
) : (
<RoundedFlag country={peer.country_code} size={20} />
)}
</div>
<div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light truncate">
<CopyToClipboardText
message={"DNS label has been copied to your clipboard"}
>
<span className={"font-normal truncate"}>{peer.dns_label}</span>
</CopyToClipboardText>
<CopyToClipboardText
message={"IP address has been copied to your clipboard"}
>
<span
className={"dark:text-nb-gray-400 font-mono font-thin text-xs"}
>
{peer.ip}
</span>
</CopyToClipboardText>
</div>
</div>
</div>
</FullTooltip>
);
}

View File

@@ -0,0 +1,74 @@
import { FlagIcon, GlobeIcon, MapPin, NetworkIcon } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
import Skeleton from "react-loading-skeleton";
import { useCountries } from "@/contexts/CountryProvider";
import { Peer } from "@/interfaces/Peer";
type Props = {
peer: Peer;
};
export const PeerAddressTooltipContent = ({ peer }: Props) => {
const { isLoading, getRegionByPeer } = useCountries();
const countryText = useMemo(() => {
return getRegionByPeer(peer);
}, [getRegionByPeer, peer]);
return (
<div
className={"text-xs flex flex-col"}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<ListItem
icon={<MapPin size={14} />}
label={"NetBird IP"}
value={peer.ip}
/>
<ListItem
icon={<NetworkIcon size={14} />}
label={"Public IP"}
value={peer.connection_ip}
/>
<ListItem
icon={<GlobeIcon size={14} />}
label={"Domain"}
value={peer.dns_label}
/>
<ListItem
icon={<FlagIcon size={14} />}
label={"Region"}
value={
isLoading && !countryText ? <Skeleton width={100} /> : countryText
}
/>
</div>
);
};
const ListItem = ({
icon,
label,
value,
}: {
icon: React.ReactNode;
label: string;
value: string | React.ReactNode;
}) => {
return (
<div
className={
"flex justify-between gap-10 border-b border-nb-gray-920 py-2 px-4 last:border-b-0"
}
>
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
{icon}
{label}
</div>
<div className={"text-nb-gray-400"}>{value}</div>
</div>
);
};

View File

@@ -171,7 +171,7 @@ const CheckContent = ({ value, onChange }: Props) => {
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink href={"#"} target={"_blank"}>
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks#geolocation-check"} target={"_blank"}>
Country & Region Check
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -86,7 +86,7 @@ const CheckContent = ({ value, onChange }: Props) => {
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink href={"#"} target={"_blank"}>
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks#net-bird-client-version-check"} target={"_blank"}>
Client Version Check
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -215,7 +215,7 @@ const CheckContent = ({ value, onChange }: Props) => {
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink href={"#"} target={"_blank"}>
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks#operating-system-version-check"} target={"_blank"}>
Operating System Check
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -0,0 +1,219 @@
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 { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
import cidr from "ip-cidr";
import { isEmpty, uniqueId } from "lodash";
import {
ExternalLinkIcon,
MinusCircleIcon,
NetworkIcon,
PlusCircle,
ShieldCheck,
ShieldXIcon,
} from "lucide-react";
import * as React from "react";
import { useMemo, useState } from "react";
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
type Props = {
value?: PeerNetworkRangeCheck;
onChange: (value: PeerNetworkRangeCheck | undefined) => void;
};
export const PostureCheckPeerNetworkRange = ({ value, onChange }: Props) => {
const [open, setOpen] = useState(false);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
icon={<NetworkIcon size={16} />}
title={"Peer Network Range"}
modalWidthClass={"max-w-xl"}
description={
"Restrict access by allowing or blocking peer network ranges."
}
iconClass={"bg-gradient-to-tr from-blue-500 to-blue-400"}
active={value !== undefined}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
/>
</PostureCheckCard>
);
};
interface NetworkRange {
id: string;
value: string;
}
const CheckContent = ({ value, onChange }: Props) => {
const [allowOrDeny, setAllowOrDeny] = useState<string>(
value?.action ? value.action : "allow",
);
const [networkRanges, setNetworkRanges] = useState<NetworkRange[]>(
value?.ranges
? value.ranges.map((r) => {
return {
id: uniqueId("range"),
value: r,
};
})
: [],
);
const handleNetworkRangeChange = (id: string, value: string) => {
const newRanges = networkRanges.map((r) =>
r.id === id ? { ...r, value } : r,
);
setNetworkRanges(newRanges);
};
const removeNetworkRange = (id: string) => {
const newRanges = networkRanges.filter((r) => r.id !== id);
setNetworkRanges(newRanges);
};
const addNetworkRange = () => {
setNetworkRanges([...networkRanges, { id: uniqueId("range"), value: "" }]);
};
const validateNetworkRange = (networkRange: string) => {
if (networkRange == "") return "";
const validCIDR = cidr.isValidAddress(networkRange);
if (!validCIDR) return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
return "";
};
const cidrErrors = useMemo(() => {
if (networkRanges && networkRanges.length > 0) {
return networkRanges.map((r) => {
return {
id: r.id,
error: validateNetworkRange(r.value),
};
});
} else {
return [];
}
}, [networkRanges]);
const hasErrorsOrIsEmpty = useMemo(() => {
if (networkRanges.length === 0) return true;
return cidrErrors.some((e) => e.error !== "");
}, [networkRanges, cidrErrors]);
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>Allow or Block Ranges</Label>
<HelpText className={""}>
Choose whether you want to allow or block specific peer network
ranges
</HelpText>
</div>
<RadioGroup value={allowOrDeny} onChange={setAllowOrDeny}>
<RadioGroupItem value={"allow"} variant={"green"}>
<ShieldCheck size={16} />
Allow
</RadioGroupItem>
<RadioGroupItem value={"deny"} variant={"red"}>
<ShieldXIcon size={16} />
Block
</RadioGroupItem>
</RadioGroup>
</div>
{networkRanges.length > 0 && (
<div className={"mb-2 flex flex-col gap-2 w-full "}>
{networkRanges.map((ipRange) => {
return (
<div key={ipRange.id} className={"flex gap-2"}>
<div className={"w-full"}>
<Input
customPrefix={<NetworkIcon size={16} />}
placeholder={"e.g., 172.16.0.0/16"}
value={ipRange.value}
error={cidrErrors.find((e) => e.id === ipRange.id)?.error}
errorTooltip={false}
className={"font-mono !text-[13px] w-full"}
onChange={(e) =>
handleNetworkRangeChange(ipRange.id, e.target.value)
}
/>
</div>
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeNetworkRange(ipRange.id)}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button variant={"dotted"} size={"sm"} onClick={addNetworkRange}>
<PlusCircle size={16} />
Add Network Range
</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#peer-network-range-check"
}
target={"_blank"}
>
Peer Network Range 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(networkRanges)) {
onChange(undefined);
} else {
onChange({
action: allowOrDeny as "allow" | "deny",
ranges: networkRanges
.map((r) => r.value)
.filter((r) => r !== ""),
});
}
}}
>
Save
</Button>
</div>
</ModalFooter>
</>
);
};

View File

@@ -0,0 +1,67 @@
import Badge from "@components/Badge";
import FullTooltip from "@components/FullTooltip";
import { ScrollArea } from "@components/ScrollArea";
import { NetworkIcon } from "lucide-react";
import * as React from "react";
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
type Props = {
check?: PeerNetworkRangeCheck;
children?: React.ReactNode;
};
export const PeerNetworkRangeTooltip = ({ check, children }: Props) => {
return check ? (
<FullTooltip
className={"w-full"}
interactive={true}
contentClassName={"p-0"}
content={
<div
className={"text-neutral-300 text-sm max-w-xs flex flex-col gap-1"}
>
<div className={"px-4 pt-3"}>
{check.action == "allow" ? (
<span>
<span className={"text-green-500 font-semibold"}>
Allow only
</span>{" "}
the following peer network ranges
</span>
) : (
<span>
<span className={"text-red-500 font-semibold"}>Block</span> the
following peer network ranges
</span>
)}
</div>
<ScrollArea
className={"max-h-[275px] overflow-y-auto flex flex-col px-4"}
>
<div className={"flex flex-col gap-1.5 mt-1 text-xs mb-3.5"}>
{check.ranges.map((ipRange, index) => {
return (
<Badge
variant={"gray"}
useHover={false}
key={index}
className={
"justify-start font-medium font-mono text-[11px]"
}
>
<NetworkIcon size={10} />
{ipRange}
</Badge>
);
})}
</div>
</ScrollArea>
</div>
}
>
{children}
</FullTooltip>
) : (
children
);
};

View File

@@ -23,6 +23,7 @@ import {
import { PostureCheckGeoLocation } from "@/modules/posture-checks/checks/PostureCheckGeoLocation";
import { PostureCheckNetBirdVersion } from "@/modules/posture-checks/checks/PostureCheckNetBirdVersion";
import { PostureCheckOperatingSystem } from "@/modules/posture-checks/checks/PostureCheckOperatingSystem";
import { PostureCheckPeerNetworkRange } from "@/modules/posture-checks/checks/PostureCheckPeerNetworkRange";
type Props = {
open: boolean;
@@ -54,6 +55,9 @@ export default function PostureCheckModal({
const [osVersionCheck, setOsVersionCheck] = useState(
postureCheck?.checks.os_version_check || undefined,
);
const [peerNetworkRangeCheck, setPeerNetworkRangeCheck] = useState(
postureCheck?.checks.peer_network_range_check || undefined,
);
const validateOSCheck = (osCheck?: OperatingSystemVersionCheck) => {
if (!osCheck) return;
@@ -93,6 +97,7 @@ export default function PostureCheckModal({
nb_version_check: nbVersionCheck,
geo_location_check: validateLocationCheck(geoLocationCheck),
os_version_check: validateOSCheck(osVersionCheck),
peer_network_range_check: peerNetworkRangeCheck,
},
};
@@ -125,7 +130,10 @@ export default function PostureCheckModal({
};
const isAtLeastOneCheckEnabled =
!!nbVersionCheck || !!geoLocationCheck || !!osVersionCheck;
!!nbVersionCheck ||
!!geoLocationCheck ||
!!osVersionCheck ||
!!peerNetworkRangeCheck;
const canCreate = !isEmpty(name) && isAtLeastOneCheckEnabled;
const [tab, setTab] = useState("checks");
@@ -180,6 +188,10 @@ export default function PostureCheckModal({
value={osVersionCheck}
onChange={setOsVersionCheck}
/>
<PostureCheckPeerNetworkRange
value={peerNetworkRangeCheck}
onChange={setPeerNetworkRangeCheck}
/>
</>
</TabsContent>
<TabsContent value={"general"} className={"pb-8 px-8"}>
@@ -221,9 +233,7 @@ export default function PostureCheckModal({
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
}
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
target={"_blank"}
>
Posture Checks

View File

@@ -174,7 +174,7 @@ export default function PostureCheckTable({ postureChecks, isLoading }: Props) {
learnMore={
<>
Learn more about
<InlineLink href={"#"} target={"_blank"}>
<InlineLink href={"https://docs.netbird.io/how-to/manage-posture-checks"} target={"_blank"}>
Posture Checks
<ExternalLinkIcon size={12} />
</InlineLink>

View File

@@ -1,11 +1,12 @@
import { cn } from "@utils/helpers";
import { Disc3Icon, FlagIcon } from "lucide-react";
import { Disc3Icon, FlagIcon, NetworkIcon } from "lucide-react";
import * as React from "react";
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
import { PostureCheck } from "@/interfaces/PostureCheck";
import { GeoLocationTooltip } from "@/modules/posture-checks/checks/tooltips/GeoLocationTooltip";
import { NetBirdVersionTooltip } from "@/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip";
import { OperatingSystemTooltip } from "@/modules/posture-checks/checks/tooltips/OperatingSystemTooltip";
import { PeerNetworkRangeTooltip } from "@/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip";
type Props = {
check: PostureCheck;
@@ -56,6 +57,20 @@ export const PostureCheckChecksCell = ({ check }: Props) => {
</div>
</OperatingSystemTooltip>
)}
{check.checks.peer_network_range_check && (
<PeerNetworkRangeTooltip
check={check.checks.peer_network_range_check}
>
<div
className={cn(
"bg-gradient-to-tr from-blue-500 to-blue-400 h-8 w-8 rounded-full flex items-center justify-center relative z-[8] hover:scale-[1.1] transition-all",
)}
>
<NetworkIcon size={14} />
</div>
</PeerNetworkRangeTooltip>
)}
</div>
</div>
</div>

View File

@@ -1,5 +1,3 @@
"use client";
import Button from "@components/Button";
import Code from "@components/Code";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
@@ -39,11 +37,12 @@ import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
type Props = {
children: React.ReactNode;
children?: React.ReactNode;
open: boolean;
setOpen: (open: boolean) => void;
};
const copyMessage = "Setup-Key was copied to your clipboard!";
export default function SetupKeyModal({ children }: Props) {
const [modal, setModal] = useState(false);
export default function SetupKeyModal({ children, open, setOpen }: Props) {
const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const [, copy] = useCopyToClipboard(setupKey?.key);
@@ -55,15 +54,15 @@ export default function SetupKeyModal({ children }: Props) {
return (
<>
<Modal open={modal} onOpenChange={setModal} key={modal ? 1 : 0}>
<ModalTrigger asChild>{children}</ModalTrigger>
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
{children && <ModalTrigger asChild>{children}</ModalTrigger>}
<SetupKeyModalContent onSuccess={handleSuccess} />
</Modal>
<Modal
open={successModal}
onOpenChange={(open) => {
setSuccessModal(open);
setModal(open);
setOpen(open);
}}
>
<ModalContent

View File

@@ -9,7 +9,7 @@ import GetStartedTest from "@components/ui/GetStartedTest";
import { SortingState } from "@tanstack/react-table";
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
import { usePathname } from "next/navigation";
import React from "react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import { useLocalStorage } from "@/hooks/useLocalStorage";
@@ -47,114 +47,126 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
},
],
);
const [open, setOpen] = useState(false);
return (
<DataTable
isLoading={isLoading}
text={"Setup Keys"}
sorting={sorting}
setSorting={setSorting}
columns={SetupKeysTableColumns}
data={setupKeys}
searchPlaceholder={"Search by name, type or group..."}
columnVisibility={{
valid: false,
group_strings: false,
}}
getStartedCard={
<GetStartedTest
icon={
<SquareIcon
icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />}
color={"gray"}
size={"large"}
/>
}
title={"Create Setup Key"}
description={
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
}
button={
<SetupKeyModal>
<Button variant={"primary"} className={""}>
<PlusCircle size={16} />
Create Setup Key
</Button>
</SetupKeyModal>
}
learnMore={
<>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
<>
<SetupKeyModal open={open} setOpen={setOpen} />
<DataTable
isLoading={isLoading}
text={"Setup Keys"}
sorting={sorting}
setSorting={setSorting}
columns={SetupKeysTableColumns}
data={setupKeys}
searchPlaceholder={"Search by name, type or group..."}
columnVisibility={{
valid: false,
group_strings: false,
}}
getStartedCard={
<GetStartedTest
icon={
<SquareIcon
icon={
<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />
}
target={"_blank"}
color={"gray"}
size={"large"}
/>
}
title={"Create Setup Key"}
description={
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
}
button={
<Button
variant={"primary"}
className={""}
onClick={() => setOpen(true)}
>
Setup Keys
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
}
rightSide={() => (
<>
{setupKeys && setupKeys?.length > 0 && (
<SetupKeyModal>
<Button variant={"primary"} className={"ml-auto"}>
<PlusCircle size={16} />
Create Setup Key
</Button>
</SetupKeyModal>
)}
</>
)}
>
{(table) => (
<>
<ButtonGroup disabled={setupKeys?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue(true);
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() == true
? "tertiary"
: "secondary"
}
>
Valid
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue("");
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() != true
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
</ButtonGroup>
<DataTableRowsPerPage
table={table}
disabled={setupKeys?.length == 0}
}
learnMore={
<>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
}
target={"_blank"}
>
Setup Keys
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
<DataTableRefreshButton
isDisabled={setupKeys?.length == 0}
onClick={() => {
mutate("/setup-keys").then();
mutate("/groups").then();
}}
/>
</>
)}
</DataTable>
}
rightSide={() => (
<>
{setupKeys && setupKeys?.length > 0 && (
<Button
variant={"primary"}
className={"ml-auto"}
onClick={() => setOpen(true)}
>
<PlusCircle size={16} />
Create Setup Key
</Button>
)}
</>
)}
>
{(table) => (
<>
<ButtonGroup disabled={setupKeys?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue(true);
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() == true
? "tertiary"
: "secondary"
}
>
Valid
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue("");
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() != true
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
</ButtonGroup>
<DataTableRowsPerPage
table={table}
disabled={setupKeys?.length == 0}
/>
<DataTableRefreshButton
isDisabled={setupKeys?.length == 0}
onClick={() => {
mutate("/setup-keys").then();
mutate("/groups").then();
}}
/>
</>
)}
</DataTable>
</>
);
}

View File

@@ -77,7 +77,11 @@ export function useNetBirdFetch(ignoreError: boolean = false) {
};
}
export default function useFetchApi<T>(url: string, ignoreError = false) {
export default function useFetchApi<T>(
url: string,
ignoreError = false,
revalidate = true,
) {
const { fetch } = useNetBirdFetch(ignoreError);
const handleErrors = useApiErrorHandling(ignoreError);
@@ -90,6 +94,9 @@ export default function useFetchApi<T>(url: string, ignoreError = false) {
},
{
keepPreviousData: true,
revalidateOnFocus: revalidate,
revalidateIfStale: revalidate,
revalidateOnReconnect: revalidate,
},
);