Compare commits

...
5 Commits
Author SHA1 Message Date
Eduard Gert 8e7bcc0c22 Extend posture checks with peer network range check (#344)
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 GertandMaycon Santos 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) 2024-02-22 21:46:55 +01:00
32 changed files with 773 additions and 173 deletions

No files matched your search

+1
View File
@@ -2,6 +2,7 @@ name: build and push
on:
push:
branches:
- "feature/**"
- main
tags:
- "**"
+52 -2
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={
<>
+1 -1
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>
+7 -1
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>
+6 -2
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}
/>
)}
+1
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}
@@ -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 -5
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) => {
+5 -2
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>
);
+1 -1
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>
+2 -1
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>
+47
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);
};
+9 -2
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>
+19 -5
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 };
};
/**
+3
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;
}
+6
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" },
+4 -1
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>
+1 -1
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) => {
+1 -1
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;
+45 -27
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>
);
}
Loaded 20 of 32 files, more files were not shown because too many files have changed in this diff. Show more