Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e7bcc0c22 | ||
|
|
02a0b71e46 | ||
|
|
a8b66d935f | ||
|
|
f74f9cf812 | ||
|
|
7578595f05 |
1
.github/workflows/build_and_push.yml
vendored
1
.github/workflows/build_and_push.yml
vendored
@@ -2,6 +2,7 @@ name: build and push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature/**"
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
36
src/components/skeletons/SkeletonPeerDetail.tsx
Normal file
36
src/components/skeletons/SkeletonPeerDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
47
src/contexts/CountryProvider.tsx
Normal file
47
src/contexts/CountryProvider.tsx
Normal 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);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
74
src/modules/peers/PeerAddressTooltipContent.tsx
Normal file
74
src/modules/peers/PeerAddressTooltipContent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user