Compare commits

...

2 Commits

Author SHA1 Message Date
Maycon Santos
b949f60afe Feature/client service expose (#567)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* add draft

* add reverse proxy activities

* move peer expose settings into client settings tab and fix activity descriptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* prevent false positive group report

* add docs link

* allow save when groups are added to the setting

* Add loading skeleton to client settings, update icon, use grouphelper to allow creating new groups, remove .patch

* mv expose settings from extra settings

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-02-24 14:54:58 +01:00
Eduard Gert
d498e4cc25 Fix dns records pagination (#566)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2026-02-20 21:42:26 +01:00
5 changed files with 177 additions and 24 deletions

View File

@@ -0,0 +1,20 @@
import * as React from "react";
import Skeleton from "react-loading-skeleton";
export const SkeletonSettings = () => {
return (
<div className={"p-default py-6 max-w-2xl"}>
<Skeleton height={24} width={200} className={"mb-6"} />
<Skeleton height={32} width={110} className={"mb-10"} />
<div className={"mb-8"}>
<Skeleton height={17} width={200} className={"mb-2"} />
<Skeleton height={80} width={"100%"} />
</div>
<div className={"mb-8"}>
<Skeleton height={17} width={200} className={"mb-2"} />
<Skeleton height={80} width={"100%"} />
</div>
<Skeleton height={80} width={"100%"} />
</div>
);
};

View File

@@ -10,6 +10,8 @@ export interface Account {
user_approval_required: boolean;
};
peer_login_expiration_enabled: boolean;
peer_expose_enabled?: boolean;
peer_expose_groups?: string[];
peer_login_expiration: number;
peer_inactivity_expiration_enabled: boolean;
peer_inactivity_expiration: number;

View File

@@ -664,6 +664,35 @@ export default function ActivityDescription({ event }: Props) {
</div>
);
/**
* Reverse Proxy
*/
if (event.activity_code == "service.peer.expose")
return (
<div className={"inline"}>
Peer <Value>{m.peer_name}</Value> exposed service{" "}
<Value>{m.domain}</Value> with auth{" "}
<Value>{m.auth ? "Enabled" : "Disabled"}</Value>
</div>
);
if (event.activity_code == "service.peer.unexpose")
return (
<div className={"inline"}>
Peer <Value>{m.peer_name}</Value> unexposed service{" "}
<Value>{m.domain}</Value>
</div>
);
if (event.activity_code == "service.peer.expose.expire")
return (
<div className={"inline"}>
Service <Value>{m.domain}</Value> exposed by peer{" "}
<Value>{m.peer_name}</Value> was removed due to renewal expiration
</div>
);
/**
* Networks
*/

View File

@@ -66,6 +66,7 @@ export default function DNSRecordsTable({ zone }: Props) {
className={"bg-nb-gray-960 py-2"}
inset={true}
text={"DNS Records"}
initialPageSize={zone?.records?.length}
manualPagination={true}
sorting={sorting}
columnVisibility={{}}

View File

@@ -6,6 +6,7 @@ import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import {
SelectDropdown,
SelectOption,
@@ -13,7 +14,7 @@ import {
import { useHasChanges } from "@hooks/useHasChanges";
import * as Tabs from "@radix-ui/react-tabs";
import { useApiCall } from "@utils/api";
import { validator } from "@utils/helpers";
import { cn, validator } from "@utils/helpers";
import {
ClockFadingIcon,
ExternalLinkIcon,
@@ -27,6 +28,10 @@ import SettingsIcon from "@/assets/icons/SettingsIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { Account } from "@/interfaces/Account";
import { SmallBadge } from "@components/ui/SmallBadge";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { useGroups } from "@/contexts/GroupsProvider";
import { SkeletonSettings } from "@components/skeletons/SkeletonSettings";
type Props = {
account: Account;
@@ -48,6 +53,16 @@ const latestOrCustomVersion = [
] as SelectOption[];
export default function ClientSettingsTab({ account }: Readonly<Props>) {
const { isLoading: isGroupsLoading } = useGroups();
return isGroupsLoading ? (
<SkeletonSettings />
) : (
<ClientSettingsTabContent account={account} />
);
}
function ClientSettingsTabContent({ account }: Readonly<Props>) {
const { permission } = usePermissions();
const { mutate } = useSWRConfig();
@@ -69,9 +84,23 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
isCustomVersion ? autoUpdateSetting : "",
);
const [peerExposeEnabled, setPeerExposeEnabled] = useState<boolean>(
account?.settings?.peer_expose_enabled ?? false,
);
const [peerExposeGroups, setPeerExposeGroups, { save: saveGroups }] =
useGroupHelper({
initial: account.settings?.peer_expose_groups,
});
const peerExposeGroupNames = useMemo(
() => peerExposeGroups.map((g) => g.name).sort(),
[peerExposeGroups],
);
const { hasChanges, updateRef } = useHasChanges([
autoUpdateMethod,
autoUpdateCustomVersion,
peerExposeEnabled,
peerExposeGroupNames,
]);
const handleUpdateMethodChange = (value: string) => {
@@ -99,16 +128,24 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
return (
!hasChanges ||
!permission.settings.update ||
(autoUpdateMethod === "custom" && !canSaveCustomVersion)
(autoUpdateMethod === "custom" && !canSaveCustomVersion) ||
(peerExposeEnabled && peerExposeGroups.length === 0)
);
}, [
hasChanges,
permission.settings.update,
autoUpdateMethod,
canSaveCustomVersion,
peerExposeEnabled,
peerExposeGroups,
]);
const saveChanges = async () => {
const groups = await saveGroups();
const peerExposeGroupIds = groups
.map((group) => group.id)
.filter(Boolean) as string[];
notify({
title: "Client Settings",
description: `Client settings successfully updated.`,
@@ -118,11 +155,18 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
settings: {
...account.settings,
auto_update_version: autoUpdateCustomVersion || autoUpdateMethod,
peer_expose_enabled: peerExposeEnabled,
peer_expose_groups: peerExposeGroupIds,
},
})
.then(() => {
mutate("/accounts");
updateRef([autoUpdateMethod, autoUpdateCustomVersion]);
updateRef([
autoUpdateMethod,
autoUpdateCustomVersion,
peerExposeEnabled,
peerExposeGroupNames,
]);
}),
loadingMessage: "Updating client settings...",
});
@@ -152,7 +196,7 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
return (
<Tabs.Content value={"clients"}>
<div className={"p-default py-6 max-w-xl"}>
<div className={"p-default py-6 max-w-2xl"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/settings"}
@@ -178,7 +222,7 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
</Button>
</div>
<div className={"flex flex-col gap-6 w-full mt-8"}>
<div className={"flex flex-col gap-10 w-full mt-8"}>
<div className={"flex flex-col relative"}>
<Label>
<RefreshCcw size={15} />
@@ -223,7 +267,63 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
</div>
</div>
<div className={"mt-3"}>
<div>
<div>
<Label>
<ReverseProxyIcon size={15} className={"fill-nb-gray-300"} />
Expose Services from CLI
</Label>
<HelpText>
Allow peers to expose local services through the NetBird reverse
proxy using the CLI. <br /> This requires at least NetBird{" "}
<span className={"text-white font-medium"}>v0.66.0</span>.{" "}
<InlineLink
href={
"https://docs.netbird.io/manage/reverse-proxy/expose-from-cli"
}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</HelpText>
</div>
<FancyToggleSwitch
className={"mt-2"}
value={peerExposeEnabled}
onChange={setPeerExposeEnabled}
label={"Enable Peer Expose"}
helpText={
"When enabled, peers can expose local HTTP services accessible via a public URL."
}
disabled={!permission.settings.update}
/>
<div
className={cn(
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
!peerExposeEnabled
? "opacity-50 pointer-events-none"
: "bg-nb-gray-930/80",
)}
>
<div className={"mt-2"}>
<Label>Allowed peer groups</Label>
<HelpText>
Select which peer groups are allowed to expose services. At
least one group is required.
</HelpText>
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
/>
</div>
</div>
</div>
<div>
<Label>
<FlaskConicalIcon size={15} />
Experimental
@@ -241,25 +341,26 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
<ExternalLinkIcon size={12} />
</InlineLink>
</HelpText>
<FancyToggleSwitch
className={"mt-2"}
value={lazyConnection}
onChange={toggleLazyConnection}
label={
<>
<ClockFadingIcon size={15} />
Enable Lazy Connections
</>
}
helpText={
<>
Allow to establish connections between peers only when
required. This requires NetBird client v0.45 or higher.
Changes will only take effect after restarting the clients.
</>
}
disabled={!permission.settings.update}
/>
</div>
<FancyToggleSwitch
value={lazyConnection}
onChange={toggleLazyConnection}
label={
<>
<ClockFadingIcon size={15} />
Enable Lazy Connections
</>
}
helpText={
<>
Allow to establish connections between peers only when required.
This requires NetBird client v0.45 or higher. Changes will only
take effect after restarting the clients.
</>
}
disabled={!permission.settings.update}
/>
</div>
</div>
</Tabs.Content>