fix(i18n): localize Peer sections and Posture Checks components

- AccessiblePeersSection, PeerNetworkRoutesSection, PeerRemoteJobsSection
- PostureCheckGeoLocation, PostureCheckNetBirdVersion, PostureCheckOperatingSystem
- PostureCheckPeerNetworkRange, PostureCheckProcess, PostureCheckNoChecksInfo
- Update en.ts and zh.ts messages with new translation keys
This commit is contained in:
sakuradairong
2026-06-23 21:25:55 +08:00
parent 055252d68b
commit 312c32f6ea
11 changed files with 1435 additions and 1342 deletions

View File

@@ -322,7 +322,10 @@ saveGroups: "Save Groups",
getStartedDescription: "It looks like you don't have any connected machines.\nGet started by adding one to your network.",
learnMoreInOur: "Learn more in our",
gettingStartedGuide: "Getting Started Guide",
userPeersDescription: "View all peers registered by this user."
userPeersDescription: "View all peers registered by this user.",
accessiblePeersDesc: "This peer can connect to the following peers within the NetBird network.",
networkRoutesDesc: "Access other networks without installing NetBird on every resource.",
remoteJobsDesc: "Remotely trigger actions such as debug bundles or other tasks on this peer, without requiring CLI access."
},
policies: {
title: "Policies",
@@ -980,7 +983,56 @@ serviceUsersDescription: "Use service users to create API tokens and avoid losin
postureCheckNameHelp: "Set an easily identifiable name for your posture check.",
postureCheckNamePlaceholder: "e.g., NetBird Version > 0.25.0",
postureCheckDescriptionHelp: "Write a short description to add more context to this policy.",
postureCheckDescriptionPlaceholder: "e.g., Check if the NetBird version is bigger than 0.25.0"
postureCheckDescriptionPlaceholder: "e.g., Check if the NetBird version is bigger than 0.25.0",
netBirdClientVersion: "NetBird Client Version",
netBirdClientVersionHelp: "Restrict access to peers with a specific NetBird client version.",
netBirdClientVersionCheck: "Client Version Check",
minimumRequiredVersion: "Minimum required version",
minimumRequiredVersionHelp: "Only peers with the minimum specified NetBird client version will have access to the network.",
minimumRequiredVersionPlaceholder: "e.g., 0.25.0",
minimumRequiredVersionError: "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1",
countryAndRegion: "Country & Region",
countryAndRegionHelp: "Restrict access in your network based on country or region.",
countryAndRegionCheck: "Country & Region Check",
geoLite2License: "This check includes GeoLite2 data created by MaxMind, available from",
allowOrBlockLocation: "Allow or Block Location",
chooseAllowOrBlock: "Choose whether you want to allow or block access from specific countries or regions",
addLocation: "Add Location",
learnMoreAbout: "Learn more about",
operatingSystemHelp: "Restrict access in your network based on the operating system.",
operatingSystemCheck: "Operating System Check",
allVersions: "All versions",
equalOrGreaterThan: "Equal or greater than",
allowOrBlock: "Allow or Block",
allowOrBlockOSHelp: "Choose whether you want to allow or block the operating system.",
selectVersion: "Select version...",
versionPlaceholder: "e.g., 6.0.0",
useCustomVersion: "Use custom version number",
useCustomVersionHelp: "Use a custom version number if you need more control.",
kernelVersion: "Kernel Version",
process: "Process",
processHelp: "Restrict access in your network based on running processes of a peer.",
processCheck: "Process Check",
processes: "Processes",
processesHelp: "Add the path of an executable file of the process. You can define a path for Linux, macOS and Windows. Peers will only be allowed to connect if the process is running on their system.",
addProcess: "Add Process",
linuxPathPlaceholder: "/usr/local/bin/netbird",
macPathPlaceholder: "/Applications/NetBird.app/Contents/MacOS/netbird",
windowsPathPlaceholder: "C:\\ProgramData\\NetBird\\netbird.exe",
validMacPath: "Please enter a valid macOS file path",
validUnixPath: "Please enter a valid Unix file path",
validWindowsPath: "Please enter a valid Windows file path",
peerNetworkRangeHelp: "Restrict access by allowing or blocking peer network ranges.",
peerNetworkRangeCheck: "Peer Network Range Check",
allowOrBlockRanges: "Allow or Block Ranges",
allowOrBlockRangesHelp: "Choose whether you want to allow or block specific peer network ranges",
addNetworkRange: "Add Network Range",
validCidr: "Please enter a valid CIDR, e.g., 192.168.1.0/24",
cidrPlaceholder: "e.g., 172.16.0.0/16",
noChecks: "You haven't added any posture checks yet",
noChecksDescription: "Add various posture checks to further restrict access in your network. E.g., only clients with a specific NetBird client version, operating system or location are allowed to connect.",
browseChecks: "Browse Checks",
newPostureCheck: "New Posture Check"
},
setupKeys: {
title: "Setup Keys",

View File

@@ -322,7 +322,10 @@ saveGroups: "保存组",
getStartedDescription: "看起来您还没有任何连接的设备。\n开始使用向您的网络中添加一台设备。",
learnMoreInOur: "在我们的",
gettingStartedGuide: "入门指南",
userPeersDescription: "查看此用户注册的所有节点。"
userPeersDescription: "查看此用户注册的所有节点。",
accessiblePeersDesc: "此节点可以连接到 NetBird 网络中的以下节点。",
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
remoteJobsDesc: "远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。"
},
policies: {
title: "策略",
@@ -980,7 +983,56 @@ disable2FA: "禁用两步验证",
postureCheckNameHelp: "为姿态检查设置一个易于识别的名称。",
postureCheckNamePlaceholder: "例如NetBird 版本 > 0.25.0",
postureCheckDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。",
postureCheckDescriptionPlaceholder: "例如:检查 NetBird 版本是否大于 0.25.0"
postureCheckDescriptionPlaceholder: "例如:检查 NetBird 版本是否大于 0.25.0",
netBirdClientVersion: "NetBird 客户端版本",
netBirdClientVersionHelp: "根据特定的 NetBird 客户端版本限制对节点的访问。",
netBirdClientVersionCheck: "客户端版本检查",
minimumRequiredVersion: "最低所需版本",
minimumRequiredVersionHelp: "仅具有指定最低 NetBird 客户端版本的对等节点才能访问网络。",
minimumRequiredVersionPlaceholder: "例如0.25.0",
minimumRequiredVersionError: "请输入有效版本例如0.2, 0.2.0, 0.2.0-alpha.1",
countryAndRegion: "国家与地区",
countryAndRegionHelp: "根据国家或地区限制网络中的访问。",
countryAndRegionCheck: "国家与地区检查",
geoLite2License: "此检查包含由 MaxMind 创建的 GeoLite2 数据,来源于",
allowOrBlockLocation: "允许或阻止位置",
chooseAllowOrBlock: "选择您希望允许或阻止来自特定国家或地区的访问",
addLocation: "添加位置",
learnMoreAbout: "了解更多关于",
operatingSystemHelp: "根据操作系统限制网络中的访问。",
operatingSystemCheck: "操作系统检查",
allVersions: "所有版本",
equalOrGreaterThan: "等于或大于",
allowOrBlock: "允许或阻止",
allowOrBlockOSHelp: "选择您希望允许或阻止的操作系统。",
selectVersion: "选择版本...",
versionPlaceholder: "例如6.0.0",
useCustomVersion: "使用自定义版本号",
useCustomVersionHelp: "如果需要更多控制,请使用自定义版本号。",
kernelVersion: "内核版本",
process: "进程",
processHelp: "根据对等节点上正在运行的进程限制网络中的访问。",
processCheck: "进程检查",
processes: "进程",
processesHelp: "添加进程的可执行文件路径。您可以为 Linux、macOS 和 Windows 定义路径。仅当进程在系统上运行时,对等节点才允许连接。",
addProcess: "添加进程",
linuxPathPlaceholder: "/usr/local/bin/netbird",
macPathPlaceholder: "/Applications/NetBird.app/Contents/MacOS/netbird",
windowsPathPlaceholder: "C:\\ProgramData\\NetBird\\netbird.exe",
validMacPath: "请输入有效的 macOS 文件路径",
validUnixPath: "请输入有效的 Unix 文件路径",
validWindowsPath: "请输入有效的 Windows 文件路径",
peerNetworkRangeHelp: "通过允许或阻止对等节点网络范围来限制访问。",
peerNetworkRangeCheck: "对等节点网络范围检查",
allowOrBlockRanges: "允许或阻止范围",
allowOrBlockRangesHelp: "选择您希望允许或阻止特定对等节点网络范围",
addNetworkRange: "添加网络范围",
validCidr: "请输入有效的 CIDR例如192.168.1.0/24",
cidrPlaceholder: "例如172.16.0.0/16",
noChecks: "您还没有添加任何态势检查",
noChecksDescription: "添加各种态势检查以进一步限制网络中的访问。例如,仅允许具有特定 NetBird 客户端版本、操作系统或位置的客户端连接。",
browseChecks: "浏览检查",
newPostureCheck: "新建态势检查"
},
setupKeys: {
title: "安装密钥",

View File

@@ -1,72 +1,74 @@
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable, {
SkeletonTableHeader,
SkeletonTableHeader,
} from "@components/skeletons/SkeletonTable";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import * as React from "react";
import { lazy, Suspense } from "react";
import { useTranslations } from "next-intl";
import { useUsers } from "@/contexts/UsersProvider";
import type { Peer } from "@/interfaces/Peer";
const AccessiblePeersTable = lazy(
() => import("@/modules/peer/MinimalPeersTable"),
() => import("@/modules/peer/MinimalPeersTable"),
);
type Props = {
peerID: string;
peerID: string;
};
export const AccessiblePeersSection = ({ peerID }: Props) => {
const { data: peers, isLoading } = useFetchApi<Peer[]>(
`/peers/${peerID}/accessible-peers`,
);
const { users } = useUsers();
const t = useTranslations("peers");
const tCommon = useTranslations("common");
const { data: peers, isLoading } = useFetchApi<Peer[]>(
`/peers/${peerID}/accessible-peers`,
);
const { users } = useUsers();
const peersWithUser = peers?.map((peer) => {
if (!users) return peer;
return {
...peer,
user: users?.find((user) => user.id === peer.user_id),
};
});
const peersWithUser = peers?.map((peer) => {
if (!users) return peer;
return {
...peer,
user: users?.find((user) => user.id === peer.user_id),
};
});
return (
<div className={"pb-10 px-8"}>
<div className={""}>
<div className={"flex justify-between items-center mb-5"}>
<div>
<Paragraph>
This peer can connect to the following peers within the NetBird
network.{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/manage-network-access"}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
</div>
return (
<div className={"pb-10 px-8"}>
<div className={""}>
<div className={"flex justify-between items-center mb-5"}>
<div>
<Paragraph>
{t("accessiblePeersDesc")}{" "}
<InlineLink
href={"https://docs.netbird.io/how-to/manage-network-access"}
target={"_blank"}
>
{tCommon("learnMore")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
</div>
<Suspense
fallback={
<div>
<SkeletonTableHeader className={"!p-0"} />
<div className={"mt-8 w-full"}>
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<AccessiblePeersTable
peerID={peerID}
isLoading={isLoading}
peers={peersWithUser}
/>
</Suspense>
</div>
</div>
);
<Suspense
fallback={
<div>
<SkeletonTableHeader className={"!p-0"} />
<div className={"mt-8 w-full"}>
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<AccessiblePeersTable
peerID={peerID}
isLoading={isLoading}
peers={peersWithUser}
/>
</Suspense>
</div>
</div>
);
};

View File

@@ -9,63 +9,65 @@ import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
import usePeerRoutes from "@/modules/peer/usePeerRoutes";
import InlineLink from "@components/InlineLink";
import { ExternalLinkIcon } from "lucide-react";
import { useTranslations } from "next-intl";
const PeerRoutesTable = lazy(() => import("@/modules/peer/PeerRoutesTable"));
type Props = {
peer: Peer;
peer: Peer;
};
export const PeerNetworkRoutesSection = ({ peer }: Props) => {
const { peerRoutes, isLoading } = usePeerRoutes({ peer });
const exitNodeInfo = useHasExitNodes(peer);
const t = useTranslations("peers");
const tCommon = useTranslations("common");
const { peerRoutes, isLoading } = usePeerRoutes({ peer });
const exitNodeInfo = useHasExitNodes(peer);
return (
<div className={"pb-10 px-8"}>
<div className={""}>
<div className={"flex justify-between items-center mb-5"}>
<div>
<Paragraph>
Access other networks without installing NetBird on every
resource.{" "}
<InlineLink
href={
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div className={"gap-4 flex"}>
<AddExitNodeButton
peer={peer}
firstTime={!exitNodeInfo.hasExitNode}
/>
<AddRouteDropdownButton />
</div>
</div>
</div>
return (
<div className={"pb-10 px-8"}>
<div className={""}>
<div className={"flex justify-between items-center mb-5"}>
<div>
<Paragraph>
{t("networkRoutesDesc")}{" "}
<InlineLink
href={
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
}
target={"_blank"}
>
{tCommon("learnMore")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"inline-flex gap-4 justify-end"}>
<div className={"gap-4 flex"}>
<AddExitNodeButton
peer={peer}
firstTime={!exitNodeInfo.hasExitNode}
/>
<AddRouteDropdownButton />
</div>
</div>
</div>
<Suspense
fallback={
<div>
<div className={"mt-0 w-full"}>
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<PeerRoutesTable
peer={peer}
isLoading={isLoading}
peerRoutes={peerRoutes}
/>
</Suspense>
</div>
</div>
);
<Suspense
fallback={
<div>
<div className={"mt-0 w-full"}>
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<PeerRoutesTable
peer={peer}
isLoading={isLoading}
peerRoutes={peerRoutes}
/>
</Suspense>
</div>
</div>
);
};

View File

@@ -1,59 +1,61 @@
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import { useTranslations } from "next-intl";
import InlineLink from "@/components/InlineLink";
import Paragraph from "@/components/Paragraph";
import SkeletonTable, {
SkeletonTableHeader,
SkeletonTableHeader,
} from "@/components/skeletons/SkeletonTable";
import { Job } from "@/interfaces/Job";
import useFetchApi from "@/utils/api";
const PeerRemoteJobsTable = lazy(
() => import("@/modules/jobs/table/PeerRemoteJobsTable"),
() => import("@/modules/jobs/table/PeerRemoteJobsTable"),
);
type Props = {
peerID: string;
peerID: string;
};
export const PeerRemoteJobsSection = ({ peerID }: Props) => {
const { data: jobs, isLoading } = useFetchApi<Job[]>(`/peers/${peerID}/jobs`);
const t = useTranslations("peers");
const tCommon = useTranslations("common");
const { data: jobs, isLoading } = useFetchApi<Job[]>(`/peers/${peerID}/jobs`);
return (
<div className="pb-10 px-8">
<div className="">
<div className="flex justify-between items-center mb-5">
<div>
<Paragraph>
Remotely trigger actions such as debug bundles or other tasks on
this peer, without requiring CLI access.{" "}
<InlineLink
href={"https://docs.netbird.io/manage/peers/remote-jobs"}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
</div>
return (
<div className="pb-10 px-8">
<div className="">
<div className="flex justify-between items-center mb-5">
<div>
<Paragraph>
{t("remoteJobsDesc")}{" "}
<InlineLink
href={"https://docs.netbird.io/manage/peers/remote-jobs"}
target={"_blank"}
>
{tCommon("learnMore")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
</div>
<Suspense
fallback={
<div>
<SkeletonTableHeader className="!p-0" />
<div className="mt-8 w-full">
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<PeerRemoteJobsTable
peerID={peerID}
jobs={jobs}
isLoading={isLoading}
/>
</Suspense>
</div>
</div>
);
<Suspense
fallback={
<div>
<SkeletonTableHeader className="!p-0" />
<div className="mt-8 w-full">
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<PeerRemoteJobsTable
peerID={peerID}
jobs={jobs}
isLoading={isLoading}
/>
</Suspense>
</div>
</div>
);
};

View File

@@ -9,12 +9,12 @@ import { CitySelector } from "@components/ui/CitySelector";
import { CountrySelector } from "@components/ui/CountrySelector";
import { isEmpty, uniqueId } from "lodash";
import {
ExternalLinkIcon,
FlagIcon,
MinusCircleIcon,
PlusCircle,
ShieldCheck,
ShieldXIcon,
ExternalLinkIcon,
FlagIcon,
MinusCircleIcon,
PlusCircle,
ShieldCheck,
ShieldXIcon,
} from "lucide-react";
import * as React from "react";
import { useState } from "react";
@@ -23,195 +23,192 @@ import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
import { useTranslations } from "next-intl";
type Props = {
value?: GeoLocationCheck;
onChange: (value: GeoLocationCheck | undefined) => void;
disabled?: boolean;
value?: GeoLocationCheck;
onChange: (value: GeoLocationCheck | undefined) => void;
disabled?: boolean;
};
export const PostureCheckGeoLocation = ({
value,
onChange,
disabled,
value,
onChange,
disabled,
}: Props) => {
const [open, setOpen] = useState(false);
const t = useTranslations("postureChecks");
const [open, setOpen] = useState(false);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
icon={<FlagIcon size={16} />}
title={"Country & Region"}
description={
"Restrict access in your network based on country or region."
}
iconClass={"bg-gradient-to-tr from-indigo-500 to-indigo-400"}
modalWidthClass={"max-w-2xl"}
active={value ? value?.locations?.length > 0 : false}
onReset={() => onChange(undefined)}
license={
<div className={"text-xs max-w-xs"}>
This check includes GeoLite2 data created by MaxMind, available from{" "}
<InlineLink href={"https://www.maxmind.com"} target={"_blank"}>
https://www.maxmind.com
</InlineLink>
</div>
}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
icon={<FlagIcon size={16} />}
title={t("countryAndRegion")}
description={t("countryAndRegionHelp")}
iconClass={"bg-gradient-to-tr from-indigo-500 to-indigo-400"}
modalWidthClass={"max-w-2xl"}
active={value ? value?.locations?.length > 0 : false}
onReset={() => onChange(undefined)}
license={
<div className={"text-xs max-w-xs"}>
{t("geoLite2License")}{" "}
<InlineLink href={"https://www.maxmind.com"} target={"_blank"}>
https://www.maxmind.com
</InlineLink>
</div>
}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
};
const CheckContent = ({ value, onChange, disabled }: Props) => {
const t = useTranslations("common");
const [allowDenyLocation, setAllowDenyLocation] = useState<string>(
value?.action ? value.action : "allow",
);
const [locations, setLocations] = useState<GeoLocation[]>(
value?.locations.map((l) => {
return {
id: uniqueId("location"),
country_code: l.country_code,
city_name: l.city_name || "",
};
}) || [],
);
const t = useTranslations("postureChecks");
const tCommon = useTranslations("common");
const [allowDenyLocation, setAllowDenyLocation] = useState<string>(
value?.action ? value.action : "allow",
);
const [locations, setLocations] = useState<GeoLocation[]>(
value?.locations.map((l) => {
return {
id: uniqueId("location"),
country_code: l.country_code,
city_name: l.city_name || "",
};
}) || [],
);
const updateLocation = (id: string, location: GeoLocation) => {
const find = locations.find((l) => l.id === id);
if (find) {
Object.assign(find, location);
setLocations([...locations]);
}
};
const updateLocation = (id: string, location: GeoLocation) => {
const find = locations.find((l) => l.id === id);
if (find) {
Object.assign(find, location);
setLocations([...locations]);
}
};
const removeLocation = (id: string) => {
setLocations(locations.filter((l) => l.id !== id));
};
const removeLocation = (id: string) => {
setLocations(locations.filter((l) => l.id !== id));
};
const addLocation = () => {
setLocations([
...locations,
{ id: uniqueId("location"), country_code: "AF", city_name: "" },
]);
};
const addLocation = () => {
setLocations([
...locations,
{ id: uniqueId("location"), country_code: "AF", city_name: "" },
]);
};
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 Location</Label>
<HelpText className={""}>
Choose whether you want to allow or block access from specific
countries or regions
</HelpText>
</div>
<RadioGroup value={allowDenyLocation} onChange={setAllowDenyLocation}>
<RadioGroupItem value={"allow"} variant={"green"}>
<ShieldCheck size={16} />
Allow
</RadioGroupItem>
<RadioGroupItem value={"deny"} variant={"red"}>
<ShieldXIcon size={16} />
Block
</RadioGroupItem>
</RadioGroup>
</div>
{locations.length > 0 && (
<div className={"mb-2 flex flex-col gap-2 w-full "}>
{locations.map((location) => {
return (
<div key={location.id} className={"flex gap-2"}>
<CountrySelector
value={location.country_code}
onChange={(value) => {
updateLocation(location.id, {
...location,
country_code: value,
});
}}
/>
{location.country_code && (
<CitySelector
value={location.city_name || ""}
onChange={(value) => {
updateLocation(location.id, {
...location,
city_name: value,
});
}}
country={location.country_code}
/>
)}
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>{t("allowOrBlockLocation")}</Label>
<HelpText className={""}>{t("chooseAllowOrBlock")}</HelpText>
</div>
<RadioGroup value={allowDenyLocation} onChange={setAllowDenyLocation}>
<RadioGroupItem value={"allow"} variant={"green"}>
<ShieldCheck size={16} />
{tCommon("allow")}
</RadioGroupItem>
<RadioGroupItem value={"deny"} variant={"red"}>
<ShieldXIcon size={16} />
{tCommon("block")}
</RadioGroupItem>
</RadioGroup>
</div>
{locations.length > 0 && (
<div className={"mb-2 flex flex-col gap-2 w-full "}>
{locations.map((location) => {
return (
<div key={location.id} className={"flex gap-2"}>
<CountrySelector
value={location.country_code}
onChange={(value) => {
updateLocation(location.id, {
...location,
country_code: value,
});
}}
/>
{location.country_code && (
<CitySelector
value={location.city_name || ""}
onChange={(value) => {
updateLocation(location.id, {
...location,
city_name: value,
});
}}
country={location.country_code}
/>
)}
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeLocation(location.id)}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button
variant={"dotted"}
size={"sm"}
disabled={allowDenyLocation == "all" || disabled}
onClick={addLocation}
>
<PlusCircle size={16} />
Add Location
</Button>
</div>
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeLocation(location.id)}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button
variant={"dotted"}
size={"sm"}
disabled={allowDenyLocation == "all" || disabled}
onClick={addLocation}
>
<PlusCircle size={16} />
{t("addLocation")}
</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#geolocation-check"
}
target={"_blank"}
>
Country & Region Check
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{t("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
onClick={() => {
if (isEmpty(locations)) {
onChange(undefined);
} else {
onChange({
action: allowDenyLocation as "allow" | "deny",
locations: locations,
});
}
}}
disabled={disabled}
>
Save
</Button>
</div>
</ModalFooter>
</>
);
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
{t("learnMoreAbout")}
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-posture-checks#geolocation-check"
}
target={"_blank"}
>
{t("countryAndRegionCheck")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{tCommon("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
onClick={() => {
if (isEmpty(locations)) {
onChange(undefined);
} else {
onChange({
action: allowDenyLocation as "allow" | "deny",
locations: locations,
});
}
}}
disabled={disabled}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</>
);
};

View File

@@ -16,120 +16,116 @@ import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
import { useTranslations } from "next-intl";
type Props = {
value?: NetBirdVersionCheck;
onChange: (value: NetBirdVersionCheck | undefined) => void;
disabled?: boolean;
value?: NetBirdVersionCheck;
onChange: (value: NetBirdVersionCheck | undefined) => void;
disabled?: boolean;
};
export const PostureCheckNetBirdVersion = ({
value,
onChange,
disabled,
value,
onChange,
disabled,
}: Props) => {
const [open, setOpen] = useState(false);
const t = useTranslations("postureChecks");
const [open, setOpen] = useState(false);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
active={value?.min_version !== undefined}
title={"NetBird Client Version"}
description={
"Restrict access to peers with a specific NetBird client version."
}
icon={<NetBirdIcon size={18} />}
modalWidthClass={"max-w-lg"}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
active={value?.min_version !== undefined}
title={t("netBirdClientVersion")}
description={t("netBirdClientVersionHelp")}
icon={<NetBirdIcon size={18} />}
modalWidthClass={"max-w-lg"}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
};
const CheckContent = ({ value, onChange, disabled }: Props) => {
const t = useTranslations("common");
const [version, setVersion] = useState(value?.min_version || "");
const t = useTranslations("postureChecks");
const tCommon = useTranslations("common");
const [version, setVersion] = useState(value?.min_version || "");
const versionError = useMemo(() => {
if (version == "") return "";
const validSemver = validator.isValidVersion(version);
if (!validSemver)
return "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
}, [version]);
const versionError = useMemo(() => {
if (version == "") return "";
const validSemver = validator.isValidVersion(version);
if (!validSemver) return t("minimumRequiredVersionError");
}, [version]);
const canSave = useMemo(() => {
return (
!versionError &&
version !== value?.min_version &&
!isEmpty(version) &&
!disabled
);
}, [version, versionError, value, disabled]);
const canSave = useMemo(() => {
return (
!versionError &&
version !== value?.min_version &&
!isEmpty(version) &&
!disabled
);
}, [version, versionError, value, disabled]);
return (
<>
<div className={"flex flex-col px-8 gap-3 pb-6"}>
<div>
<Label>Minimum required version</Label>
<HelpText>
Only peers with the minimum specified NetBird client version will
have access to the network.
</HelpText>
<div>
<Input
className={"max-w-[200px]"}
value={version}
onChange={(e) => setVersion(e.target.value)}
placeholder={"e.g., 0.25.0"}
error={versionError}
customPrefix={"Version"}
disabled={disabled}
/>
</div>
</div>
</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#net-bird-client-version-check"
}
target={"_blank"}
>
Client Version Check
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{t("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={!canSave}
onClick={() => {
if (isEmpty(version)) {
onChange(undefined);
} else {
onChange({ min_version: version });
}
}}
>
Save
</Button>
</div>
</ModalFooter>
</>
);
return (
<>
<div className={"flex flex-col px-8 gap-3 pb-6"}>
<div>
<Label>{t("minimumRequiredVersion")}</Label>
<HelpText>{t("minimumRequiredVersionHelp")}</HelpText>
<div>
<Input
className={"max-w-[200px]"}
value={version}
onChange={(e) => setVersion(e.target.value)}
placeholder={t("minimumRequiredVersionPlaceholder")}
error={versionError}
customPrefix={t("version")}
disabled={disabled}
/>
</div>
</div>
</div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
{t("learnMoreAbout")}
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-posture-checks#net-bird-client-version-check"
}
target={"_blank"}
>
{t("netBirdClientVersionCheck")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{tCommon("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={!canSave}
onClick={() => {
if (isEmpty(version)) {
onChange(undefined);
} else {
onChange({ min_version: version });
}
}}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</>
);
};

View File

@@ -8,20 +8,20 @@ import { ModalClose, ModalFooter } from "@components/modal/Modal";
import Paragraph from "@components/Paragraph";
import { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
import {
SelectDropdown,
SelectOption,
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import { IconMathEqualGreater } from "@tabler/icons-react";
import { validator } from "@utils/helpers";
import { isEmpty } from "lodash";
import {
Disc3Icon,
ExternalLinkIcon,
FileCog,
GalleryHorizontalEnd,
ShieldCheck,
ShieldXIcon,
Disc3Icon,
ExternalLinkIcon,
FileCog,
GalleryHorizontalEnd,
ShieldCheck,
ShieldXIcon,
} from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
@@ -32,410 +32,409 @@ import { LinuxIcon } from "@/assets/icons/LinuxIcon";
import WindowsIcon from "@/assets/icons/WindowsIcon";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import {
androidVersions,
iOSVersions,
macOSVersions,
OperatingSystemVersionCheck,
windowsKernelVersions,
androidVersions,
iOSVersions,
macOSVersions,
OperatingSystemVersionCheck,
windowsKernelVersions,
} from "@/interfaces/PostureCheck";
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
import { useTranslations } from "next-intl";
type Props = {
value?: OperatingSystemVersionCheck;
onChange: (value: OperatingSystemVersionCheck | undefined) => void;
disabled?: boolean;
value?: OperatingSystemVersionCheck;
onChange: (value: OperatingSystemVersionCheck | undefined) => void;
disabled?: boolean;
};
export const PostureCheckOperatingSystem = ({
value,
onChange,
disabled,
value,
onChange,
disabled,
}: Props) => {
const [open, setOpen] = useState(false);
const t = useTranslations("postureChecks");
const [open, setOpen] = useState(false);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
icon={<Disc3Icon size={16} />}
title={"Operating System"}
modalWidthClass={"max-w-xl"}
description={
"Restrict access in your network based on the operating system."
}
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
active={value !== undefined}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
icon={<Disc3Icon size={16} />}
title={t("operatingSystem")}
modalWidthClass={"max-w-xl"}
description={t("operatingSystemHelp")}
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
active={value !== undefined}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
};
const CheckContent = ({ value, onChange, disabled }: Props) => {
const t = useTranslations("common");
const [tab] = useState(String(OperatingSystem.LINUX));
const t = useTranslations("postureChecks");
const tCommon = useTranslations("common");
const [tab] = useState(String(OperatingSystem.LINUX));
const firstTimeCheck = value === undefined;
const firstTimeCheck = value === undefined;
const [windowsVersion, setWindowsVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.windows
? value.windows.min_kernel_version
: "-",
);
const [macOSVersion, setMacOSVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.darwin
? value.darwin?.min_version
: "-",
);
const [androidVersion, setAndroidVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.android
? value.android?.min_version
: "-",
);
const [iOSVersion, setIOSVersion] = useState<string>(
firstTimeCheck ? "" : value && value.ios ? value.ios?.min_version : "-",
);
const [linuxVersion, setLinuxVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.linux
? value.linux?.min_kernel_version
: "-",
);
const [windowsVersion, setWindowsVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.windows
? value.windows.min_kernel_version
: "-",
);
const [macOSVersion, setMacOSVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.darwin
? value.darwin?.min_version
: "-",
);
const [androidVersion, setAndroidVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.android
? value.android?.min_version
: "-",
);
const [iOSVersion, setIOSVersion] = useState<string>(
firstTimeCheck ? "" : value && value.ios ? value.ios?.min_version : "-",
);
const [linuxVersion, setLinuxVersion] = useState<string>(
firstTimeCheck
? ""
: value && value.linux
? value.linux?.min_kernel_version
: "-",
);
const [linuxError, setLinuxError] = useState("");
const [windowsError, setWindowsError] = useState("");
const [macOSError, setMacOSError] = useState("");
const [iOSError, setIOSError] = useState("");
const [androidError, setAndroidError] = useState("");
const [linuxError, setLinuxError] = useState("");
const [windowsError, setWindowsError] = useState("");
const [macOSError, setMacOSError] = useState("");
const [iOSError, setIOSError] = useState("");
const [androidError, setAndroidError] = useState("");
const versionError =
linuxError ||
windowsError ||
macOSError ||
iOSError ||
androidError ||
disabled;
const versionError =
linuxError ||
windowsError ||
macOSError ||
iOSError ||
androidError ||
disabled;
return (
<>
<Tabs defaultValue={tab}>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={String(OperatingSystem.LINUX)}>
<LinuxIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Linux
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
<WindowsIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Windows
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.APPLE)}>
<AppleIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
macOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.IOS)}>
<IOSIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
iOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
<AndroidIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Android
</TabsTrigger>
</TabsList>
<TabsContent value={String(OperatingSystem.LINUX)} className={"px-8"}>
<OperatingSystemTab
value={linuxVersion}
onChange={setLinuxVersion}
os={OperatingSystem.LINUX}
onError={setLinuxError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.WINDOWS)} className={"px-8"}>
<OperatingSystemTab
versionList={windowsKernelVersions}
value={windowsVersion}
onChange={setWindowsVersion}
os={OperatingSystem.WINDOWS}
onError={setWindowsError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.APPLE)} className={"px-8"}>
<OperatingSystemTab
versionList={macOSVersions}
value={macOSVersion}
onChange={setMacOSVersion}
os={OperatingSystem.APPLE}
onError={setMacOSError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.IOS)} className={"px-8"}>
<OperatingSystemTab
versionList={iOSVersions}
value={iOSVersion}
onChange={setIOSVersion}
os={OperatingSystem.IOS}
onError={setIOSError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.ANDROID)} className={"px-8"}>
<OperatingSystemTab
versionList={androidVersions}
value={androidVersion}
onChange={setAndroidVersion}
os={OperatingSystem.ANDROID}
onError={setAndroidError}
disabled={disabled}
/>
</TabsContent>
</Tabs>
<div className={"h-6"}></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#operating-system-version-check"
}
target={"_blank"}
>
Operating System Check
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{t("cancel")}</Button>
</ModalClose>
<Button
disabled={!!versionError}
variant={"primary"}
onClick={() => {
const osCheck = {} as OperatingSystemVersionCheck;
return (
<>
<Tabs defaultValue={tab}>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={String(OperatingSystem.LINUX)}>
<LinuxIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Linux
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
<WindowsIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Windows
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.APPLE)}>
<AppleIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
macOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.IOS)}>
<IOSIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
iOS
</TabsTrigger>
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
<AndroidIcon
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
Android
</TabsTrigger>
</TabsList>
<TabsContent value={String(OperatingSystem.LINUX)} className={"px-8"}>
<OperatingSystemTab
value={linuxVersion}
onChange={setLinuxVersion}
os={OperatingSystem.LINUX}
onError={setLinuxError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.WINDOWS)} className={"px-8"}>
<OperatingSystemTab
versionList={windowsKernelVersions}
value={windowsVersion}
onChange={setWindowsVersion}
os={OperatingSystem.WINDOWS}
onError={setWindowsError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.APPLE)} className={"px-8"}>
<OperatingSystemTab
versionList={macOSVersions}
value={macOSVersion}
onChange={setMacOSVersion}
os={OperatingSystem.APPLE}
onError={setMacOSError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.IOS)} className={"px-8"}>
<OperatingSystemTab
versionList={iOSVersions}
value={iOSVersion}
onChange={setIOSVersion}
os={OperatingSystem.IOS}
onError={setIOSError}
disabled={disabled}
/>
</TabsContent>
<TabsContent value={String(OperatingSystem.ANDROID)} className={"px-8"}>
<OperatingSystemTab
versionList={androidVersions}
value={androidVersion}
onChange={setAndroidVersion}
os={OperatingSystem.ANDROID}
onError={setAndroidError}
disabled={disabled}
/>
</TabsContent>
</Tabs>
<div className={"h-6"}></div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
{t("learnMoreAbout")}
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-posture-checks#operating-system-version-check"
}
target={"_blank"}
>
{t("operatingSystemCheck")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{tCommon("cancel")}</Button>
</ModalClose>
<Button
disabled={!!versionError}
variant={"primary"}
onClick={() => {
const osCheck = {} as OperatingSystemVersionCheck;
if (windowsVersion !== "-") {
osCheck.windows = { min_kernel_version: windowsVersion };
}
if (macOSVersion !== "-") {
osCheck.darwin = { min_version: macOSVersion };
}
if (androidVersion !== "-") {
osCheck.android = { min_version: androidVersion };
}
if (iOSVersion !== "-") {
osCheck.ios = { min_version: iOSVersion };
}
if (linuxVersion !== "-") {
osCheck.linux = { min_kernel_version: linuxVersion };
}
if (windowsVersion !== "-") {
osCheck.windows = { min_kernel_version: windowsVersion };
}
if (macOSVersion !== "-") {
osCheck.darwin = { min_version: macOSVersion };
}
if (androidVersion !== "-") {
osCheck.android = { min_version: androidVersion };
}
if (iOSVersion !== "-") {
osCheck.ios = { min_version: iOSVersion };
}
if (linuxVersion !== "-") {
osCheck.linux = { min_kernel_version: linuxVersion };
}
if (isEmpty(osCheck)) {
onChange(undefined);
} else {
onChange(osCheck);
}
}}
>
Save
</Button>
</div>
</ModalFooter>
</>
);
if (isEmpty(osCheck)) {
onChange(undefined);
} else {
onChange(osCheck);
}
}}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</>
);
};
type OperatingSystemTabProps = {
value: string;
onChange: (value: string) => void;
versionList?: SelectOption[];
os: OperatingSystem;
onError: (error: string) => void;
disabled?: boolean;
value: string;
onChange: (value: string) => void;
versionList?: SelectOption[];
os: OperatingSystem;
onError: (error: string) => void;
disabled?: boolean;
};
const allOrMinOptions = [
{
label: "All versions",
value: "all",
icon: GalleryHorizontalEnd,
},
{
label: "Equal or greater than",
value: "min",
icon: IconMathEqualGreater,
},
] as SelectOption[];
export const OperatingSystemTab = ({
value,
onChange,
versionList,
os,
onError,
disabled,
value,
onChange,
versionList,
os,
onError,
disabled,
}: OperatingSystemTabProps) => {
const [allow, setAllow] = useState(value == "-" ? "block" : "allow");
const [allOrMin, setAllOrMin] = useState(
value == "" || value == "-" || value == "0" ? "all" : "min",
);
const [useCustomVersion, setUseCustomVersion] = useState(() => {
if (!versionList) return false;
if (!value) return false;
if (value === "-") return false;
if (value === "0") return false;
const find = versionList.map((v) => v.value).includes(value);
return !find;
});
const t = useTranslations("postureChecks");
const tCommon = useTranslations("common");
const allOrMinOptions: SelectOption[] = [
{
label: t("allVersions"),
value: "all",
icon: GalleryHorizontalEnd,
},
{
label: t("equalOrGreaterThan"),
value: "min",
icon: IconMathEqualGreater,
},
];
const [allow, setAllow] = useState(value == "-" ? "block" : "allow");
const [allOrMin, setAllOrMin] = useState(
value == "" || value == "-" || value == "0" ? "all" : "min",
);
const [useCustomVersion, setUseCustomVersion] = useState(() => {
if (!versionList) return false;
if (!value) return false;
if (value === "-") return false;
if (value === "0") return false;
const find = versionList.map((v) => v.value).includes(value);
return !find;
});
const changeAllow = (value: string) => {
setAllow(value);
if (value === "block") {
setAllOrMin("all");
onChange("-");
setAllOrMin("all");
setUseCustomVersion(false);
} else {
onChange("");
setAllOrMin("all");
setUseCustomVersion(false);
}
};
const changeAllow = (value: string) => {
setAllow(value);
if (value === "block") {
setAllOrMin("all");
onChange("-");
setAllOrMin("all");
setUseCustomVersion(false);
} else {
onChange("");
setAllOrMin("all");
setUseCustomVersion(false);
}
};
const changeAllOrMin = (option: string) => {
setAllOrMin(option);
if (option === "all") {
onChange("");
} else if (option === "min" && value == "" && versionList) {
const getLast = versionList[versionList.length - 1];
onChange(getLast.value);
}
};
const changeAllOrMin = (option: string) => {
setAllOrMin(option);
if (option === "all") {
onChange("");
} else if (option === "min" && value == "" && versionList) {
const getLast = versionList[versionList.length - 1];
onChange(getLast.value);
}
};
const prefix =
os === OperatingSystem.LINUX || os === OperatingSystem.WINDOWS
? "Kernel Version"
: "Version";
const prefix =
os === OperatingSystem.LINUX || os === OperatingSystem.WINDOWS
? t("kernelVersion")
: tCommon("version");
const versionError = useMemo(() => {
const msg = "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
if (value == "") return "";
if (value == "-") return "";
const validSemver = validator.isValidVersion(value);
if (!validSemver) return msg;
return "";
}, [value]);
const versionError = useMemo(() => {
const msg = t("minimumRequiredVersionError");
if (value == "") return "";
if (value == "-") return "";
const validSemver = validator.isValidVersion(value);
if (!validSemver) return msg;
return "";
}, [value, t]);
useEffect(() => {
onError(versionError);
}, [versionError, onError]);
useEffect(() => {
onError(versionError);
}, [versionError, onError]);
return (
<div className={""}>
<div className={"flex justify-between items-start gap-10 "}>
<div>
<Label>Allow or Block</Label>
<HelpText>
Choose whether you want to allow or block the operating system.
</HelpText>
</div>
<RadioGroup value={allow} onChange={changeAllow}>
<RadioGroupItem value={"allow"} variant={"green"}>
<ShieldCheck size={14} />
Allow
</RadioGroupItem>
<RadioGroupItem value={"block"} variant={"red"}>
<ShieldXIcon size={14} />
Block
</RadioGroupItem>
</RadioGroup>
</div>
<div className={"gap-4 items-center grid grid-cols-2 mt-3"}>
<SelectDropdown
value={allOrMin}
onChange={changeAllOrMin}
options={allOrMinOptions}
disabled={allow === "block" || disabled}
/>
{versionList && !useCustomVersion ? (
<SelectDropdown
value={value || "0"}
showSearch={true}
placeholder={"Select version..."}
onChange={onChange}
options={versionList}
disabled={allOrMin === "all" || allow === "block" || disabled}
/>
) : (
<Input
value={value}
customPrefix={prefix}
placeholder={"e.g., 6.0.0"}
error={versionError}
errorTooltip={true}
disabled={allOrMin === "all" || allow === "block" || disabled}
onChange={(v) => {
onChange(v.target.value);
}}
/>
)}
</div>
{os !== OperatingSystem.LINUX && (
<div className={"mt-4"}>
<FancyToggleSwitch
disabled={allow === "block" || allOrMin === "all" || disabled}
value={useCustomVersion}
onChange={setUseCustomVersion}
label={
<>
<FileCog size={14} />
Use custom version number
</>
}
helpText={"Use a custom version number if you need more control."}
/>
</div>
)}
</div>
);
return (
<div className={""}>
<div className={"flex justify-between items-start gap-10 "}>
<div>
<Label>{t("allowOrBlock")}</Label>
<HelpText>{t("allowOrBlockOSHelp")}</HelpText>
</div>
<RadioGroup value={allow} onChange={changeAllow}>
<RadioGroupItem value={"allow"} variant={"green"}>
<ShieldCheck size={14} />
{tCommon("allow")}
</RadioGroupItem>
<RadioGroupItem value={"block"} variant={"red"}>
<ShieldXIcon size={14} />
{tCommon("block")}
</RadioGroupItem>
</RadioGroup>
</div>
<div className={"gap-4 items-center grid grid-cols-2 mt-3"}>
<SelectDropdown
value={allOrMin}
onChange={changeAllOrMin}
options={allOrMinOptions}
disabled={allow === "block" || disabled}
/>
{versionList && !useCustomVersion ? (
<SelectDropdown
value={value || "0"}
showSearch={true}
placeholder={t("selectVersion")}
onChange={onChange}
options={versionList}
disabled={allOrMin === "all" || allow === "block" || disabled}
/>
) : (
<Input
value={value}
customPrefix={prefix}
placeholder={t("versionPlaceholder")}
error={versionError}
errorTooltip={true}
disabled={allOrMin === "all" || allow === "block" || disabled}
onChange={(v) => {
onChange(v.target.value);
}}
/>
)}
</div>
{os !== OperatingSystem.LINUX && (
<div className={"mt-4"}>
<FancyToggleSwitch
disabled={allow === "block" || allOrMin === "all" || disabled}
value={useCustomVersion}
onChange={setUseCustomVersion}
label={
<>
<FileCog size={14} />
{t("useCustomVersion")}
</>
}
helpText={t("useCustomVersionHelp")}
/>
</div>
)}
</div>
);
};

View File

@@ -9,12 +9,12 @@ import { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
import cidr from "ip-cidr";
import { isEmpty, uniqueId } from "lodash";
import {
ExternalLinkIcon,
MinusCircleIcon,
NetworkIcon,
PlusCircle,
ShieldCheck,
ShieldXIcon,
ExternalLinkIcon,
MinusCircleIcon,
NetworkIcon,
PlusCircle,
ShieldCheck,
ShieldXIcon,
} from "lucide-react";
import * as React from "react";
import { useMemo, useState } from "react";
@@ -23,212 +23,209 @@ import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
import { useTranslations } from "next-intl";
type Props = {
value?: PeerNetworkRangeCheck;
onChange: (value: PeerNetworkRangeCheck | undefined) => void;
disabled?: boolean;
value?: PeerNetworkRangeCheck;
onChange: (value: PeerNetworkRangeCheck | undefined) => void;
disabled?: boolean;
};
export const PostureCheckPeerNetworkRange = ({
value,
onChange,
disabled,
value,
onChange,
disabled,
}: Props) => {
const [open, setOpen] = useState(false);
const t = useTranslations("postureChecks");
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);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
icon={<NetworkIcon size={16} />}
title={t("peerNetworkRange")}
modalWidthClass={"max-w-xl"}
description={t("peerNetworkRangeHelp")}
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);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
};
interface NetworkRange {
id: string;
value: string;
id: string;
value: string;
}
const CheckContent = ({ value, onChange, disabled }: Props) => {
const t = useTranslations("common");
const [allowOrDeny, setAllowOrDeny] = useState<string>(
value?.action ? value.action : "allow",
);
const t = useTranslations("postureChecks");
const tCommon = useTranslations("common");
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 [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 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 removeNetworkRange = (id: string) => {
const newRanges = networkRanges.filter((r) => r.id !== id);
setNetworkRanges(newRanges);
};
const addNetworkRange = () => {
setNetworkRanges([...networkRanges, { id: uniqueId("range"), value: "" }]);
};
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 validateNetworkRange = (networkRange: string) => {
if (networkRange == "") return "";
const validCIDR = cidr.isValidAddress(networkRange);
if (!validCIDR) return t("validCidr");
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 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]);
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)
}
disabled={disabled}
/>
</div>
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>{t("allowOrBlockRanges")}</Label>
<HelpText className={""}>{t("allowOrBlockRangesHelp")}</HelpText>
</div>
<RadioGroup value={allowOrDeny} onChange={setAllowOrDeny}>
<RadioGroupItem value={"allow"} variant={"green"}>
<ShieldCheck size={16} />
{tCommon("allow")}
</RadioGroupItem>
<RadioGroupItem value={"deny"} variant={"red"}>
<ShieldXIcon size={16} />
{tCommon("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={t("cidrPlaceholder")}
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)
}
disabled={disabled}
/>
</div>
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeNetworkRange(ipRange.id)}
disabled={disabled}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button
variant={"dotted"}
size={"sm"}
onClick={addNetworkRange}
disabled={disabled}
>
<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"}>{t("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={hasErrorsOrIsEmpty || disabled}
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>
</>
);
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeNetworkRange(ipRange.id)}
disabled={disabled}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button
variant={"dotted"}
size={"sm"}
onClick={addNetworkRange}
disabled={disabled}
>
<PlusCircle size={16} />
{t("addNetworkRange")}
</Button>
</div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
{t("learnMoreAbout")}
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-posture-checks#peer-network-range-check"
}
target={"_blank"}
>
{t("peerNetworkRangeCheck")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{tCommon("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={hasErrorsOrIsEmpty || disabled}
onClick={() => {
if (isEmpty(networkRanges)) {
onChange(undefined);
} else {
onChange({
action: allowOrDeny as "allow" | "deny",
ranges: networkRanges
.map((r) => r.value)
.filter((r) => r !== ""),
});
}
}}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</>
);
};

View File

@@ -8,11 +8,11 @@ import Paragraph from "@components/Paragraph";
import { cn, validator } from "@utils/helpers";
import { isEmpty, uniqueId } from "lodash";
import {
ExternalLinkIcon,
MinusCircleIcon,
PlusCircle,
ServerCogIcon,
TerminalIcon,
ExternalLinkIcon,
MinusCircleIcon,
PlusCircle,
ServerCogIcon,
TerminalIcon,
} from "lucide-react";
import * as React from "react";
import { useMemo, useState } from "react";
@@ -23,297 +23,291 @@ import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
import { useTranslations } from "next-intl";
type Props = {
value?: ProcessCheck;
onChange: (value: ProcessCheck | undefined) => void;
disabled?: boolean;
value?: ProcessCheck;
onChange: (value: ProcessCheck | undefined) => void;
disabled?: boolean;
};
export const PostureCheckProcess = ({ value, onChange, disabled }: Props) => {
const [open, setOpen] = useState(false);
const t = useTranslations("postureChecks");
const [open, setOpen] = useState(false);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
active={value?.processes && value?.processes?.length > 0}
title={"Process"}
description={
"Restrict access in your network based on running processes of a peer."
}
icon={<ServerCogIcon size={18} />}
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
modalWidthClass={"max-w-xl"}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
return (
<PostureCheckCard
open={open}
setOpen={setOpen}
key={open ? 1 : 0}
active={value?.processes && value?.processes?.length > 0}
title={t("process")}
description={t("processHelp")}
icon={<ServerCogIcon size={18} />}
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
modalWidthClass={"max-w-xl"}
onReset={() => onChange(undefined)}
>
<CheckContent
value={value}
onChange={(v) => {
onChange(v);
setOpen(false);
}}
disabled={disabled}
/>
</PostureCheckCard>
);
};
const CheckContent = ({ value, onChange, disabled }: Props) => {
const t = useTranslations("common");
const [processes, setProcesses] = useState<Process[]>(
value?.processes
? value.processes.map((p) => {
return {
id: uniqueId("process"),
linux_path: p?.linux_path || "",
mac_path: p?.mac_path || "",
windows_path: p?.windows_path || "",
};
})
: [
{
id: uniqueId("process"),
linux_path: "",
mac_path: "",
windows_path: "",
},
],
);
const t = useTranslations("postureChecks");
const tCommon = useTranslations("common");
const [processes, setProcesses] = useState<Process[]>(
value?.processes
? value.processes.map((p) => {
return {
id: uniqueId("process"),
linux_path: p?.linux_path || "",
mac_path: p?.mac_path || "",
windows_path: p?.windows_path || "",
};
})
: [
{
id: uniqueId("process"),
linux_path: "",
mac_path: "",
windows_path: "",
},
],
);
const handleProcessChange = (
id: string,
linux_path: string,
mac_path: string,
windows_path: string,
) => {
const newProcesses = processes.map((p) =>
p.id === id ? { ...p, linux_path, mac_path, windows_path } : p,
);
setProcesses(newProcesses);
};
const handleProcessChange = (
id: string,
linux_path: string,
mac_path: string,
windows_path: string,
) => {
const newProcesses = processes.map((p) =>
p.id === id ? { ...p, linux_path, mac_path, windows_path } : p,
);
setProcesses(newProcesses);
};
const removeProcess = (id: string) => {
const newProcesses = processes.filter((p) => p.id !== id);
setProcesses(newProcesses);
};
const removeProcess = (id: string) => {
const newProcesses = processes.filter((p) => p.id !== id);
setProcesses(newProcesses);
};
const addProcess = () => {
setProcesses([
...processes,
{
id: uniqueId("process"),
linux_path: "",
mac_path: "",
windows_path: "",
},
]);
};
const addProcess = () => {
setProcesses([
...processes,
{
id: uniqueId("process"),
linux_path: "",
mac_path: "",
windows_path: "",
},
]);
};
const pathErrors = useMemo(() => {
if (processes && processes.length > 0) {
return processes.map((p) => {
return {
id: p.id,
errorMacPath: p?.mac_path
? validator.isValidUnixFilePath(p?.mac_path || "")
? ""
: "Please enter a valid macOS file path"
: "",
errorLinuxPath: p?.linux_path
? validator.isValidUnixFilePath(p?.linux_path || "")
? ""
: "Please enter a valid Unix file path"
: "",
errorWindowsPath: p?.windows_path
? validator.isValidWindowsFilePath(p?.windows_path || "")
? ""
: "Please enter a valid Windows file path"
: "",
};
});
} else {
return [];
}
}, [processes]);
const pathErrors = useMemo(() => {
if (processes && processes.length > 0) {
return processes.map((p) => {
return {
id: p.id,
errorMacPath: p?.mac_path
? validator.isValidUnixFilePath(p?.mac_path || "")
? ""
: t("validMacPath")
: "",
errorLinuxPath: p?.linux_path
? validator.isValidUnixFilePath(p?.linux_path || "")
? ""
: t("validUnixPath")
: "",
errorWindowsPath: p?.windows_path
? validator.isValidWindowsFilePath(p?.windows_path || "")
? ""
: t("validWindowsPath")
: "",
};
});
} else {
return [];
}
}, [processes, t]);
const hasErrorsOrIsEmpty = useMemo(() => {
if (processes.length === 0) return true;
const hasOnlyEmptyPaths = processes.some(
(p) => p.linux_path === "" && p.mac_path === "" && p.windows_path === "",
);
const hasPathErrors = pathErrors.some(
(e) =>
e.errorLinuxPath !== "" ||
e.errorMacPath !== "" ||
e.errorWindowsPath !== "",
);
return hasOnlyEmptyPaths || hasPathErrors;
}, [processes, pathErrors]);
const hasErrorsOrIsEmpty = useMemo(() => {
if (processes.length === 0) return true;
const hasOnlyEmptyPaths = processes.some(
(p) => p.linux_path === "" && p.mac_path === "" && p.windows_path === "",
);
const hasPathErrors = pathErrors.some(
(e) =>
e.errorLinuxPath !== "" ||
e.errorMacPath !== "" ||
e.errorWindowsPath !== "",
);
return hasOnlyEmptyPaths || hasPathErrors;
}, [processes, pathErrors]);
return (
<>
<div className={"flex flex-col px-8 gap-2 pb-6"}>
<div className={"flex justify-between items-start gap-10 mt-2"}>
<div>
<Label>Processes</Label>
<HelpText className={""}>
Add the path of an executable file of the process. You can define
a path for Linux, macOS and Windows. Peers will only be allowed to
connect if the process is running on their system.
</HelpText>
</div>
</div>
{processes.length > 0 && (
<div className={"mb-2 flex flex-col gap-4 w-full "}>
{processes.map((p) => {
return (
<div key={p.id} className={"flex gap-2 items-center"}>
<div className={"w-full flex flex-col gap-1.5"}>
<Input
customPrefix={<TerminalIcon size={16} />}
placeholder={"/usr/local/bin/netbird"}
value={p.linux_path}
error={
pathErrors.find((e) => e.id === p.id)?.errorLinuxPath
}
errorTooltip={true}
errorTooltipPosition={"top-right"}
className={"w-full"}
onChange={(e) =>
handleProcessChange(
p.id,
e.target.value,
p?.mac_path || "",
p?.windows_path || "",
)
}
disabled={disabled}
/>
<Input
customPrefix={
<AppleIcon
size={16}
className={cn(
pathErrors.find((e) => e.id === p.id)
?.errorMacPath && "fill-red-500",
)}
/>
}
placeholder={
"/Applications/NetBird.app/Contents/MacOS/netbird"
}
value={p.mac_path}
error={
pathErrors.find((e) => e.id === p.id)?.errorMacPath
}
errorTooltip={true}
errorTooltipPosition={"top-right"}
className={"w-full"}
onChange={(e) =>
handleProcessChange(
p.id,
p?.linux_path || "",
e.target.value,
p?.windows_path || "",
)
}
disabled={disabled}
/>
<Input
customPrefix={
<WindowsIcon
size={16}
className={cn(
pathErrors.find((e) => e.id === p.id)
?.errorWindowsPath && "fill-red-500",
)}
/>
}
placeholder={`C:\\ProgramData\\NetBird\\netbird.exe`}
value={p.windows_path}
errorTooltip={true}
errorTooltipPosition={"top-right"}
error={
pathErrors.find((e) => e.id === p.id)?.errorWindowsPath
}
className={"w-full"}
onChange={(e) =>
handleProcessChange(
p.id,
p?.linux_path || "",
p?.mac_path || "",
e.target.value,
)
}
disabled={disabled}
/>
</div>
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>{t("processes")}</Label>
<HelpText className={""}>{t("processesHelp")}</HelpText>
</div>
</div>
{processes.length > 0 && (
<div className={"mb-2 flex flex-col gap-4 w-full "}>
{processes.map((p) => {
return (
<div key={p.id} className={"flex gap-2 items-center"}>
<div className={"w-full flex flex-col gap-1.5"}>
<Input
customPrefix={<TerminalIcon size={16} />}
placeholder={t("linuxPathPlaceholder")}
value={p.linux_path}
error={
pathErrors.find((e) => e.id === p.id)?.errorLinuxPath
}
errorTooltip={true}
errorTooltipPosition={"top-right"}
className={"w-full"}
onChange={(e) =>
handleProcessChange(
p.id,
e.target.value,
p?.mac_path || "",
p?.windows_path || "",
)
}
disabled={disabled}
/>
<Input
customPrefix={
<AppleIcon
size={16}
className={cn(
pathErrors.find((e) => e.id === p.id)
?.errorMacPath && "fill-red-500",
)}
/>
}
placeholder={t("macPathPlaceholder")}
value={p.mac_path}
error={
pathErrors.find((e) => e.id === p.id)?.errorMacPath
}
errorTooltip={true}
errorTooltipPosition={"top-right"}
className={"w-full"}
onChange={(e) =>
handleProcessChange(
p.id,
p?.linux_path || "",
e.target.value,
p?.windows_path || "",
)
}
disabled={disabled}
/>
<Input
customPrefix={
<WindowsIcon
size={16}
className={cn(
pathErrors.find((e) => e.id === p.id)
?.errorWindowsPath && "fill-red-500",
)}
/>
}
placeholder={t("windowsPathPlaceholder")}
value={p.windows_path}
errorTooltip={true}
errorTooltipPosition={"top-right"}
error={
pathErrors.find((e) => e.id === p.id)?.errorWindowsPath
}
className={"w-full"}
onChange={(e) =>
handleProcessChange(
p.id,
p?.linux_path || "",
p?.mac_path || "",
e.target.value,
)
}
disabled={disabled}
/>
</div>
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeProcess(p.id)}
disabled={disabled}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button
variant={"dotted"}
size={"sm"}
onClick={addProcess}
className={"mt-1"}
disabled={disabled}
>
<PlusCircle size={16} />
Add Process
</Button>
</div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-posture-checks#process-check"
}
target={"_blank"}
>
Process Check
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{t("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={hasErrorsOrIsEmpty || disabled}
onClick={() => {
if (isEmpty(processes)) {
onChange(undefined);
} else {
onChange({
processes: processes.filter(
(p) =>
p.linux_path !== "" ||
p.mac_path !== "" ||
p.windows_path !== "",
),
});
}
}}
>
Save
</Button>
</div>
</ModalFooter>
</>
);
<Button
className={"h-[42px]"}
variant={"default-outline"}
onClick={() => removeProcess(p.id)}
disabled={disabled}
>
<MinusCircleIcon size={15} />
</Button>
</div>
);
})}
</div>
)}
<Button
variant={"dotted"}
size={"sm"}
onClick={addProcess}
className={"mt-1"}
disabled={disabled}
>
<PlusCircle size={16} />
{t("addProcess")}
</Button>
</div>
<ModalFooter className={"items-center"}>
<div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}>
{t("learnMoreAbout")}
<InlineLink
href={
"https://docs.netbird.io/how-to/manage-posture-checks#process-check"
}
target={"_blank"}
>
{t("processCheck")}
<ExternalLinkIcon size={12} />
</InlineLink>
</Paragraph>
</div>
<div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}>
<Button variant={"secondary"}>{tCommon("cancel")}</Button>
</ModalClose>
<Button
variant={"primary"}
disabled={hasErrorsOrIsEmpty || disabled}
onClick={() => {
if (isEmpty(processes)) {
onChange(undefined);
} else {
onChange({
processes: processes.filter(
(p) =>
p.linux_path !== "" ||
p.mac_path !== "" ||
p.windows_path !== "",
),
});
}
}}
>
{tCommon("save")}
</Button>
</div>
</ModalFooter>
</>
);
};

View File

@@ -5,61 +5,61 @@ import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { FolderSearch } from "lucide-react";
import * as React from "react";
import { useTranslations } from "next-intl";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { PostureCheck } from "@/interfaces/PostureCheck";
export function PostureCheckNoChecksInfo({
onAddClick,
onBrowseClick,
onAddClick,
onBrowseClick,
}: {
onAddClick: () => void;
onBrowseClick: () => void;
onAddClick: () => void;
onBrowseClick: () => void;
}) {
const { permission } = usePermissions();
const t = useTranslations("postureChecks");
const { permission } = usePermissions();
const { data: postureChecks } =
useFetchApi<PostureCheck[]>("/posture-checks");
const { data: postureChecks } =
useFetchApi<PostureCheck[]>("/posture-checks");
return (
<div>
<div
className={
"mx-auto text-center flex flex-col items-center justify-center"
}
>
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>
{"You haven't added any posture checks yet"}
</h2>
<Paragraph className={cn("text-sm text-center max-w-md mt-1")}>
Add various posture checks to further restrict access in your network.
E.g., only clients with a specific NetBird client version, operating
system or location are allowed to connect.
</Paragraph>
</div>
<div className={"flex items-center justify-center gap-4 mt-5"}>
<Button
variant={"secondary"}
size={"xs"}
disabled={
postureChecks?.length == 0 ||
!permission.policies.create ||
!permission.policies.update
}
onClick={onBrowseClick}
>
<FolderSearch size={14} />
Browse Checks
</Button>
<Button
variant={"primary"}
size={"xs"}
onClick={onAddClick}
disabled={!permission.policies.create || !permission.policies.update}
>
<IconCirclePlus size={14} />
New Posture Check
</Button>
</div>
</div>
);
return (
<div>
<div
className={
"mx-auto text-center flex flex-col items-center justify-center"
}
>
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>
{t("noChecks")}
</h2>
<Paragraph className={cn("text-sm text-center max-w-md mt-1")}>
{t("noChecksDescription")}
</Paragraph>
</div>
<div className={"flex items-center justify-center gap-4 mt-5"}>
<Button
variant={"secondary"}
size={"xs"}
disabled={
postureChecks?.length == 0 ||
!permission.policies.create ||
!permission.policies.update
}
onClick={onBrowseClick}
>
<FolderSearch size={14} />
{t("browseChecks")}
</Button>
<Button
variant={"primary"}
size={"xs"}
onClick={onAddClick}
disabled={!permission.policies.create || !permission.policies.update}
>
<IconCirclePlus size={14} />
{t("newPostureCheck")}
</Button>
</div>
</div>
);
}