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.", getStartedDescription: "It looks like you don't have any connected machines.\nGet started by adding one to your network.",
learnMoreInOur: "Learn more in our", learnMoreInOur: "Learn more in our",
gettingStartedGuide: "Getting Started Guide", 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: { policies: {
title: "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.", postureCheckNameHelp: "Set an easily identifiable name for your posture check.",
postureCheckNamePlaceholder: "e.g., NetBird Version > 0.25.0", postureCheckNamePlaceholder: "e.g., NetBird Version > 0.25.0",
postureCheckDescriptionHelp: "Write a short description to add more context to this policy.", 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: { setupKeys: {
title: "Setup Keys", title: "Setup Keys",

View File

@@ -322,7 +322,10 @@ saveGroups: "保存组",
getStartedDescription: "看起来您还没有任何连接的设备。\n开始使用向您的网络中添加一台设备。", getStartedDescription: "看起来您还没有任何连接的设备。\n开始使用向您的网络中添加一台设备。",
learnMoreInOur: "在我们的", learnMoreInOur: "在我们的",
gettingStartedGuide: "入门指南", gettingStartedGuide: "入门指南",
userPeersDescription: "查看此用户注册的所有节点。" userPeersDescription: "查看此用户注册的所有节点。",
accessiblePeersDesc: "此节点可以连接到 NetBird 网络中的以下节点。",
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
remoteJobsDesc: "远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。"
}, },
policies: { policies: {
title: "策略", title: "策略",
@@ -980,7 +983,56 @@ disable2FA: "禁用两步验证",
postureCheckNameHelp: "为姿态检查设置一个易于识别的名称。", postureCheckNameHelp: "为姿态检查设置一个易于识别的名称。",
postureCheckNamePlaceholder: "例如NetBird 版本 > 0.25.0", postureCheckNamePlaceholder: "例如NetBird 版本 > 0.25.0",
postureCheckDescriptionHelp: "写一个简短的描述为此策略添加更多上下文。", 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: { setupKeys: {
title: "安装密钥", title: "安装密钥",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,12 @@ import { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
import cidr from "ip-cidr"; import cidr from "ip-cidr";
import { isEmpty, uniqueId } from "lodash"; import { isEmpty, uniqueId } from "lodash";
import { import {
ExternalLinkIcon, ExternalLinkIcon,
MinusCircleIcon, MinusCircleIcon,
NetworkIcon, NetworkIcon,
PlusCircle, PlusCircle,
ShieldCheck, ShieldCheck,
ShieldXIcon, ShieldXIcon,
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
@@ -23,212 +23,209 @@ import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
type Props = { type Props = {
value?: PeerNetworkRangeCheck; value?: PeerNetworkRangeCheck;
onChange: (value: PeerNetworkRangeCheck | undefined) => void; onChange: (value: PeerNetworkRangeCheck | undefined) => void;
disabled?: boolean; disabled?: boolean;
}; };
export const PostureCheckPeerNetworkRange = ({ export const PostureCheckPeerNetworkRange = ({
value, value,
onChange, onChange,
disabled, disabled,
}: Props) => { }: Props) => {
const [open, setOpen] = useState(false); const t = useTranslations("postureChecks");
const [open, setOpen] = useState(false);
return ( return (
<PostureCheckCard <PostureCheckCard
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
key={open ? 1 : 0} key={open ? 1 : 0}
icon={<NetworkIcon size={16} />} icon={<NetworkIcon size={16} />}
title={"Peer Network Range"} title={t("peerNetworkRange")}
modalWidthClass={"max-w-xl"} modalWidthClass={"max-w-xl"}
description={ description={t("peerNetworkRangeHelp")}
"Restrict access by allowing or blocking peer network ranges." iconClass={"bg-gradient-to-tr from-blue-500 to-blue-400"}
} active={value !== undefined}
iconClass={"bg-gradient-to-tr from-blue-500 to-blue-400"} onReset={() => onChange(undefined)}
active={value !== undefined} >
onReset={() => onChange(undefined)} <CheckContent
> value={value}
<CheckContent onChange={(v) => {
value={value} onChange(v);
onChange={(v) => { setOpen(false);
onChange(v); }}
setOpen(false); disabled={disabled}
}} />
disabled={disabled} </PostureCheckCard>
/> );
</PostureCheckCard>
);
}; };
interface NetworkRange { interface NetworkRange {
id: string; id: string;
value: string; value: string;
} }
const CheckContent = ({ value, onChange, disabled }: Props) => { const CheckContent = ({ value, onChange, disabled }: Props) => {
const t = useTranslations("common"); const t = useTranslations("postureChecks");
const [allowOrDeny, setAllowOrDeny] = useState<string>( const tCommon = useTranslations("common");
value?.action ? value.action : "allow", const [allowOrDeny, setAllowOrDeny] = useState<string>(
); value?.action ? value.action : "allow",
);
const [networkRanges, setNetworkRanges] = useState<NetworkRange[]>( const [networkRanges, setNetworkRanges] = useState<NetworkRange[]>(
value?.ranges value?.ranges
? value.ranges.map((r) => { ? value.ranges.map((r) => {
return { return {
id: uniqueId("range"), id: uniqueId("range"),
value: r, value: r,
}; };
}) })
: [], : [],
); );
const handleNetworkRangeChange = (id: string, value: string) => { const handleNetworkRangeChange = (id: string, value: string) => {
const newRanges = networkRanges.map((r) => const newRanges = networkRanges.map((r) =>
r.id === id ? { ...r, value } : r, r.id === id ? { ...r, value } : r,
); );
setNetworkRanges(newRanges); setNetworkRanges(newRanges);
}; };
const removeNetworkRange = (id: string) => { const removeNetworkRange = (id: string) => {
const newRanges = networkRanges.filter((r) => r.id !== id); const newRanges = networkRanges.filter((r) => r.id !== id);
setNetworkRanges(newRanges); setNetworkRanges(newRanges);
}; };
const addNetworkRange = () => { const addNetworkRange = () => {
setNetworkRanges([...networkRanges, { id: uniqueId("range"), value: "" }]); setNetworkRanges([...networkRanges, { id: uniqueId("range"), value: "" }]);
}; };
const validateNetworkRange = (networkRange: string) => { const validateNetworkRange = (networkRange: string) => {
if (networkRange == "") return ""; if (networkRange == "") return "";
const validCIDR = cidr.isValidAddress(networkRange); const validCIDR = cidr.isValidAddress(networkRange);
if (!validCIDR) return "Please enter a valid CIDR, e.g., 192.168.1.0/24"; if (!validCIDR) return t("validCidr");
return ""; return "";
}; };
const cidrErrors = useMemo(() => { const cidrErrors = useMemo(() => {
if (networkRanges && networkRanges.length > 0) { if (networkRanges && networkRanges.length > 0) {
return networkRanges.map((r) => { return networkRanges.map((r) => {
return { return {
id: r.id, id: r.id,
error: validateNetworkRange(r.value), error: validateNetworkRange(r.value),
}; };
}); });
} else { } else {
return []; return [];
} }
}, [networkRanges]); }, [networkRanges]);
const hasErrorsOrIsEmpty = useMemo(() => { const hasErrorsOrIsEmpty = useMemo(() => {
if (networkRanges.length === 0) return true; if (networkRanges.length === 0) return true;
return cidrErrors.some((e) => e.error !== ""); return cidrErrors.some((e) => e.error !== "");
}, [networkRanges, cidrErrors]); }, [networkRanges, cidrErrors]);
return ( return (
<> <>
<div className={"flex flex-col px-8 gap-2 pb-6"}> <div className={"flex flex-col px-8 gap-2 pb-6"}>
<div className={"flex justify-between items-start gap-10 mt-2"}> <div className={"flex justify-between items-start gap-10 mt-2"}>
<div> <div>
<Label>Allow or Block Ranges</Label> <Label>{t("allowOrBlockRanges")}</Label>
<HelpText className={""}> <HelpText className={""}>{t("allowOrBlockRangesHelp")}</HelpText>
Choose whether you want to allow or block specific peer network </div>
ranges <RadioGroup value={allowOrDeny} onChange={setAllowOrDeny}>
</HelpText> <RadioGroupItem value={"allow"} variant={"green"}>
</div> <ShieldCheck size={16} />
<RadioGroup value={allowOrDeny} onChange={setAllowOrDeny}> {tCommon("allow")}
<RadioGroupItem value={"allow"} variant={"green"}> </RadioGroupItem>
<ShieldCheck size={16} /> <RadioGroupItem value={"deny"} variant={"red"}>
Allow <ShieldXIcon size={16} />
</RadioGroupItem> {tCommon("block")}
<RadioGroupItem value={"deny"} variant={"red"}> </RadioGroupItem>
<ShieldXIcon size={16} /> </RadioGroup>
Block </div>
</RadioGroupItem> {networkRanges.length > 0 && (
</RadioGroup> <div className={"mb-2 flex flex-col gap-2 w-full "}>
</div> {networkRanges.map((ipRange) => {
{networkRanges.length > 0 && ( return (
<div className={"mb-2 flex flex-col gap-2 w-full "}> <div key={ipRange.id} className={"flex gap-2"}>
{networkRanges.map((ipRange) => { <div className={"w-full"}>
return ( <Input
<div key={ipRange.id} className={"flex gap-2"}> customPrefix={<NetworkIcon size={16} />}
<div className={"w-full"}> placeholder={t("cidrPlaceholder")}
<Input value={ipRange.value}
customPrefix={<NetworkIcon size={16} />} error={cidrErrors.find((e) => e.id === ipRange.id)?.error}
placeholder={"e.g., 172.16.0.0/16"} errorTooltip={false}
value={ipRange.value} className={"font-mono !text-[13px] w-full"}
error={cidrErrors.find((e) => e.id === ipRange.id)?.error} onChange={(e) =>
errorTooltip={false} handleNetworkRangeChange(ipRange.id, e.target.value)
className={"font-mono !text-[13px] w-full"} }
onChange={(e) => disabled={disabled}
handleNetworkRangeChange(ipRange.id, e.target.value) />
} </div>
disabled={disabled}
/>
</div>
<Button <Button
className={"h-[42px]"} className={"h-[42px]"}
variant={"default-outline"} variant={"default-outline"}
onClick={() => removeNetworkRange(ipRange.id)} onClick={() => removeNetworkRange(ipRange.id)}
disabled={disabled} disabled={disabled}
> >
<MinusCircleIcon size={15} /> <MinusCircleIcon size={15} />
</Button> </Button>
</div> </div>
); );
})} })}
</div> </div>
)} )}
<Button <Button
variant={"dotted"} variant={"dotted"}
size={"sm"} size={"sm"}
onClick={addNetworkRange} onClick={addNetworkRange}
disabled={disabled} disabled={disabled}
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
Add Network Range {t("addNetworkRange")}
</Button> </Button>
</div> </div>
<ModalFooter className={"items-center"}> <ModalFooter className={"items-center"}>
<div className={"w-full"}> <div className={"w-full"}>
<Paragraph className={"text-sm mt-auto"}> <Paragraph className={"text-sm mt-auto"}>
Learn more about {t("learnMoreAbout")}
<InlineLink <InlineLink
href={ href={
"https://docs.netbird.io/how-to/manage-posture-checks#peer-network-range-check" "https://docs.netbird.io/how-to/manage-posture-checks#peer-network-range-check"
} }
target={"_blank"} target={"_blank"}
> >
Peer Network Range Check {t("peerNetworkRangeCheck")}
<ExternalLinkIcon size={12} /> <ExternalLinkIcon size={12} />
</InlineLink> </InlineLink>
</Paragraph> </Paragraph>
</div> </div>
<div className={"flex gap-3 w-full justify-end"}> <div className={"flex gap-3 w-full justify-end"}>
<ModalClose asChild={true}> <ModalClose asChild={true}>
<Button variant={"secondary"}>{t("cancel")}</Button> <Button variant={"secondary"}>{tCommon("cancel")}</Button>
</ModalClose> </ModalClose>
<Button <Button
variant={"primary"} variant={"primary"}
disabled={hasErrorsOrIsEmpty || disabled} disabled={hasErrorsOrIsEmpty || disabled}
onClick={() => { onClick={() => {
if (isEmpty(networkRanges)) { if (isEmpty(networkRanges)) {
onChange(undefined); onChange(undefined);
} else { } else {
onChange({ onChange({
action: allowOrDeny as "allow" | "deny", action: allowOrDeny as "allow" | "deny",
ranges: networkRanges ranges: networkRanges
.map((r) => r.value) .map((r) => r.value)
.filter((r) => r !== ""), .filter((r) => r !== ""),
}); });
} }
}} }}
> >
Save {tCommon("save")}
</Button> </Button>
</div> </div>
</ModalFooter> </ModalFooter>
</> </>
); );
}; };

View File

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

View File

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