Skip to content

Comments

Feature/client service expose#567

Open
mlsmaycon wants to merge 7 commits intomainfrom
feature/client-service-expose
Open

Feature/client service expose#567
mlsmaycon wants to merge 7 commits intomainfrom
feature/client-service-expose

Conversation

@mlsmaycon
Copy link
Contributor

@mlsmaycon mlsmaycon commented Feb 21, 2026

Summary

  • Add peer expose settings (enable/disable toggle + allowed peer groups) to the Clients settings tab, removing the standalone "Peer Expose" tab
  • Add activity descriptions for peer expose/unexpose/expiration events in the audit log
  • Extend the Account interface with peer_expose_enabled and peer_expose_groups fields

Changes

Settings - Peer Expose in Client Settings Tab

  • Moved peer expose toggle and group selector into ClientSettingsTab alongside auto-update and lazy connection settings
  • Single "Save Changes" button now persists all client settings including peer expose
  • Animated group selector appears when peer expose is enabled
  • Removed standalone PeerExposeTab component and its sidebar tab entry

Activity Log

  • service.peer.expose — "Peer X exposed service Y with auth Enabled/Disabled"
  • service.peer.unexpose — "Peer X unexposed service Y"
  • service.peer.expose.expire — "Service Y exposed by peer X was removed due to renewal expiration"
image

Summary by CodeRabbit

  • New Features

    • Comprehensive DNS Zones & Records management (create/edit/delete) with list, record UIs and icons.
    • Peer Expose controls in client settings with group-based allowlist.
  • Activity

    • Activity entries now display peer expose/unexpose/expiration details.
  • Navigation

    • DNS Zones added under DNS navigation for easier access.
  • Refactor

    • UI/path reorganizations to integrate DNS zones/nameservers and group usage.
  • Style

    • Minor color/token and layout tweaks for table/UI elements.

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

Adds DNS Zones & Records management (types, provider, CRUD, tables, modals, icon), refactors DNS module paths, integrates zones into navigation and group details, and introduces Peer Expose settings and activity-log rendering for peer expose events.

Changes

Cohort / File(s) Summary
DNS Types & Constants
src/interfaces/DNS.ts
New exported types/constants: DNSZone, DNSRecord, DNSRecordType, DNS_ZONE_DOCS_LINK, DNS_RECORDS_DOCS_LINK.
DNS Zones Provider & Hooks
src/modules/dns/zones/DNSZonesProvider.tsx
New provider and useDNSZones hook implementing zone/record CRUD and state management.
DNS Zones Table & Cells
src/modules/dns/zones/table/DNSZonesTable.tsx, src/modules/dns/zones/table/DNSZonesActionCell.tsx, src/modules/dns/zones/table/DNSZonesActiveCell.tsx, src/modules/dns/zones/table/DNSZonesNameCell.tsx, src/modules/dns/zones/table/DNSZonesRecordsCell.tsx, src/modules/dns/zones/table/DNSZonesGroupCell.tsx, src/modules/dns/zones/table/DNSZonesSearchDomainCell.tsx
Full zones table UI with per-column cell components and row actions.
DNS Records Table & Cells
src/modules/dns/zones/records/DNSRecordsTable.tsx, src/modules/dns/zones/records/DNSRecordActionCell.tsx, src/modules/dns/zones/records/DNSRecordContentCell.tsx, src/modules/dns/zones/records/DNSRecordNameCell.tsx, src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx, src/modules/dns/zones/records/DNSRecordTypeCell.tsx
Records table and per-record cell components with actions.
Zone & Record Modals
src/modules/dns/zones/DNSZoneModal.tsx, src/modules/dns/zones/DNSRecordModal.tsx
Create/edit modals and exported modal content types/hooks.
Icons & UI Utilities
src/assets/icons/DNSZoneIcon.tsx, src/modules/ui/..., src/modules/dns/...
New DNSZone icon and supporting UI utilities (clipboard helper, table helpers, modal components).
Navigation & Path Refactors
src/Navigation.tsx, src/modules/dns/...
Adds "Zones" to DNS navigation and refactors module paths (e.g., dns/nameservers, dns/zones) with updated imports across modules.
Group Integration
src/modules/groups/details/..., src/modules/dns/...
Group details and hooks updated to include DNS zones usage and a Group DNS Zones section.
Account Model
src/interfaces/Account.ts
Adds peer_expose_enabled?: boolean and peer_expose_groups?: string[] under Account.settings.extra.
Peer Expose UI
src/modules/settings/ClientSettingsTab.tsx
Adds Peer Expose toggle, PeerGroupSelector, animated collapsible panel, state handling, validation, and persistence of new account settings.
Activity Description
src/modules/activity/ActivityDescription.tsx
Adds rendering for service.peer.expose, service.peer.unexpose, and service.peer.expose.expire activity codes (duplicated branches in two rendering locations).
Styling & Tokens
src/styles/...
Minor Tailwind/color token adjustments and small UI styling tweaks.
Other Refactors & Imports
src/...
Multiple path/import adjustments from previous dns-nameservers layout to dns/nameservers and dns/zones; various components updated accordingly.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as "DNS Zones UI"
    participant Provider as "DNSZonesProvider"
    participant API as "Backend API"
    participant Modal as "DNSZone/DNSRecord Modal"
    User->>UI: open Zones page
    UI->>Provider: useDNSZones()/init
    Provider->>API: GET /zones
    API-->>Provider: zones list
    Provider-->>UI: supply zones data
    UI->>User: render zones table
    User->>UI: click Create/Edit
    UI->>Modal: open modal
    User->>Modal: submit form
    Modal->>Provider: create/update zone or record
    Provider->>API: POST/PUT /zones or /records
    API-->>Provider: created/updated entity
    Provider-->>UI: refresh list
    UI->>User: show updated data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Add DNS zones #528: Implements the same DNS zones feature set (types, provider, UI, modals, and path refactors).

Suggested reviewers

  • lixmal
  • bcmmbaga

Poem

🐇
I hopped through zones and records bright,
Tables, modals, toggles in moonlight,
Peer-expose switches, groups in line,
Records, icons, and a refreshed design,
Hooray — a rabbit's tiny DNS delight! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Feature/client service expose' is vague and generic, using a branch-naming convention rather than a clear, descriptive summary of the actual changes. Use a more specific title that describes the feature being added, such as 'Add peer expose settings to Client Settings tab' or 'Implement peer expose configuration UI'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/client-service-expose

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/modules/settings/ClientSettingsTab.tsx (1)

138-166: Peer expose groups are persisted even when peer expose is disabled.

When peerExposeEnabled is false, the save still sends peer_expose_groups: peerExposeGroupIds. This is likely harmless if the backend ignores groups when the feature is disabled, but it could be more intentional to clear or omit them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 138 - 166, The
saveChanges handler currently always includes peer_expose_groups in the payload
even when peerExposeEnabled is false; update the payload in saveRequest.put
(inside saveChanges / notify) to conditionally omit or clear groups by setting
peer_expose_groups to either peerExposeGroupIds when peerExposeEnabled is true
or to an empty array/undefined when false, and make the corresponding update to
updateRef to pass the cleared value too so the local state and mutation reflect
the disabled state.
custom-zones.patch (2)

1190-1199: Stale currentZone / currentRecord when opening modals for new items.

openZoneModal only sets currentZone when a zone is passed, and openRecordModal only sets currentRecord when a record is passed. If a previous edit left these state variables populated and the user opens a "create new" flow before the close handler clears them, the modal could display stale data.

This is likely safe in practice because the close handler clears state, but a defensive reset would be more robust:

Suggested fix
   const openZoneModal = (zone?: DNSZone, distributionGroups?: Group[]) => {
-    if (zone) setCurrentZone(zone);
-    if (distributionGroups) setInitialDistributionGroups(distributionGroups);
+    setCurrentZone(zone);
+    setInitialDistributionGroups(distributionGroups);
     setDnsModal(true);
   };

   const openRecordModal = (zone: DNSZone, record?: DNSRecord) => {
     setCurrentZone(zone);
-    if (record) setCurrentRecord(record);
+    setCurrentRecord(record);
     setRecordModal(true);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom-zones.patch` around lines 1190 - 1199, openZoneModal and
openRecordModal only set currentZone/currentRecord when a value is passed,
leaving previous state if called for "create new"; update openZoneModal to
always call setCurrentZone(zone ?? undefined) (or explicitly clear when no zone)
and always call setInitialDistributionGroups(distributionGroups ?? [] or
undefined) before setDnsModal(true), and update openRecordModal to always call
setCurrentRecord(record ?? undefined) (clearing stale record) before
setRecordModal(true), using the existing setters setCurrentZone,
setInitialDistributionGroups, setCurrentRecord, setDnsModal and setRecordModal.

2275-2276: Inconsistent truthy check for zones_count vs other count fields.

Other count fields use explicit > 0 comparisons (e.g., row.routes_count > 0, row.setup_keys_count > 0), but zones_count relies on implicit truthiness. While functionally equivalent for numeric values, this inconsistency could mask issues if zones_count is ever undefined.

Suggested fix for consistency
-        row.resources_count > 0 ||
-        row.zones_count
+        row.resources_count > 0 ||
+        row.zones_count > 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom-zones.patch` around lines 2275 - 2276, The conditional mixes explicit
numeric checks (e.g., row.resources_count > 0, row.routes_count > 0,
row.setup_keys_count > 0) with an implicit truthy check for row.zones_count;
change the implicit check to an explicit numeric comparison (row.zones_count >
0) so all count fields use consistent > 0 checks and avoid false positives when
zones_count is undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@custom-zones.patch`:
- Around line 304-305: DNS_RECORDS_DOCS_LINK is incorrectly set to the same URL
as DNS_ZONE_DOCS_LINK; update DNS_RECORDS_DOCS_LINK to point to the specific DNS
records documentation (for example replace
"https://docs.netbird.io/manage/dns/zones" with
"https://docs.netbird.io/manage/dns/zones#records" or the correct records page).
Locate the exported constant DNS_RECORDS_DOCS_LINK in the diff and change its
value to the appropriate records-specific URL so the two constants reference
distinct, correct docs pages.
- Around line 2343-2345: zonesCount can be undefined because zonesGroups may be
undefined; change the computation of zonesCount (derived from zonesGroups and
group.id) to always produce a number (e.g., default to 0) before assigning to
zones_count: ensure you call filter only when zonesGroups is defined or use a
safe fallback like (zonesGroups ? zonesGroups.filter(...) : []) and then take
.length so zonesCount is always a number; update the reference in the object
that sets zones_count to use this numeric zonesCount.

In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 77-102: The false-positive comes from initializing
peerExposeGroups as [] before initialGroups loads; fix by initializing state
from initialGroups (const [peerExposeGroups, setPeerExposeGroups] =
useState<Group[]>(initialGroups ?? [])) and in the React.useEffect that runs
when initialGroups changes, setPeerExposeGroups(initialGroups) and then call
updateRef() from useHasChanges to reset the baseline so hasChanges doesn't flip
true on load; ensure peerExposeGroupIds useMemo stays derived from
peerExposeGroups.

---

Nitpick comments:
In `@custom-zones.patch`:
- Around line 1190-1199: openZoneModal and openRecordModal only set
currentZone/currentRecord when a value is passed, leaving previous state if
called for "create new"; update openZoneModal to always call setCurrentZone(zone
?? undefined) (or explicitly clear when no zone) and always call
setInitialDistributionGroups(distributionGroups ?? [] or undefined) before
setDnsModal(true), and update openRecordModal to always call
setCurrentRecord(record ?? undefined) (clearing stale record) before
setRecordModal(true), using the existing setters setCurrentZone,
setInitialDistributionGroups, setCurrentRecord, setDnsModal and setRecordModal.
- Around line 2275-2276: The conditional mixes explicit numeric checks (e.g.,
row.resources_count > 0, row.routes_count > 0, row.setup_keys_count > 0) with an
implicit truthy check for row.zones_count; change the implicit check to an
explicit numeric comparison (row.zones_count > 0) so all count fields use
consistent > 0 checks and avoid false positives when zones_count is undefined.

In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 138-166: The saveChanges handler currently always includes
peer_expose_groups in the payload even when peerExposeEnabled is false; update
the payload in saveRequest.put (inside saveChanges / notify) to conditionally
omit or clear groups by setting peer_expose_groups to either peerExposeGroupIds
when peerExposeEnabled is true or to an empty array/undefined when false, and
make the corresponding update to updateRef to pass the cleared value too so the
local state and mutation reflect the disabled state.

Comment on lines +304 to +305
+export const DNS_ZONE_DOCS_LINK = "https://docs.netbird.io/manage/dns/zones";
+export const DNS_RECORDS_DOCS_LINK = "https://docs.netbird.io/manage/dns/zones";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DNS_RECORDS_DOCS_LINK has the same URL as DNS_ZONE_DOCS_LINK.

Both constants point to "https://docs.netbird.io/manage/dns/zones". DNS_RECORDS_DOCS_LINK should likely point to a more specific URL for DNS records documentation (e.g., https://docs.netbird.io/manage/dns/zones#records or a separate page).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom-zones.patch` around lines 304 - 305, DNS_RECORDS_DOCS_LINK is
incorrectly set to the same URL as DNS_ZONE_DOCS_LINK; update
DNS_RECORDS_DOCS_LINK to point to the specific DNS records documentation (for
example replace "https://docs.netbird.io/manage/dns/zones" with
"https://docs.netbird.io/manage/dns/zones#records" or the correct records page).
Locate the exported constant DNS_RECORDS_DOCS_LINK in the diff and change its
value to the appropriate records-specific URL so the two constants reference
distinct, correct docs pages.

Comment on lines +2343 to +2345
+ const zonesCount = zonesGroups?.filter((zone) => {
+ return zone.includes(group.id as string);
+ }).length;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

zonesCount can be undefined, but zones_count is typed as number.

When zonesGroups is undefined (during loading), zonesGroups?.filter(...) returns undefined, and .length is never called. This means zonesCount is undefined, which gets assigned to zones_count: number — a type mismatch.

Suggested fix
       const zonesCount = zonesGroups?.filter((zone) => {
         return zone.includes(group.id as string);
-      }).length;
+      }).length ?? 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ const zonesCount = zonesGroups?.filter((zone) => {
+ return zone.includes(group.id as string);
+ }).length;
const zonesCount = zonesGroups?.filter((zone) => {
return zone.includes(group.id as string);
}).length ?? 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom-zones.patch` around lines 2343 - 2345, zonesCount can be undefined
because zonesGroups may be undefined; change the computation of zonesCount
(derived from zonesGroups and group.id) to always produce a number (e.g.,
default to 0) before assigning to zones_count: ensure you call filter only when
zonesGroups is defined or use a safe fallback like (zonesGroups ?
zonesGroups.filter(...) : []) and then take .length so zonesCount is always a
number; update the reference in the object that sets zones_count to use this
numeric zonesCount.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 100-113: The useEffect that reads initialGroups and calls
setPeerExposeGroups/updateRef references autoUpdateMethod,
autoUpdateCustomVersion, peerExposeEnabled, and updateRef but intentionally
omits them from the dependency array; add an eslint suppression comment to avoid
react-hooks/exhaustive-deps warnings: place a single-line comment like //
eslint-disable-next-line react-hooks/exhaustive-deps immediately above the
React.useEffect(...) (which uses initialGroups, setPeerExposeGroups, and
updateRef) so the linter knows the omission is intentional.
- Around line 338-367: The direct child of AnimatePresence (the outer wrapper
div rendered when peerExposeEnabled is true) must include a unique React key so
Framer Motion can track enter/exit; update the JSX that renders the wrapper div
(the element containing className "overflow-hidden -top-4 relative z-0") to
include a stable key (e.g. key="peer-expose" or derived from peerExposeEnabled)
while leaving the inner motion.div and PeerGroupSelector, setPeerExposeGroups,
and peerExposeGroups logic unchanged.

Comment on lines +100 to +113
React.useEffect(() => {
if (initialGroups) {
setPeerExposeGroups(initialGroups);
const groupIds = initialGroups
.map((g) => g.id)
.filter(Boolean) as string[];
updateRef([
autoUpdateMethod,
autoUpdateCustomVersion,
peerExposeEnabled,
groupIds,
]);
}
}, [initialGroups]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing eslint-disable comment may cause lint CI failure.

autoUpdateMethod, autoUpdateCustomVersion, peerExposeEnabled, and updateRef are used inside the effect but intentionally omitted from the dependency array (adding them would reset the baseline on every user interaction). Without a suppression comment, react-hooks/exhaustive-deps will warn or error depending on the project's ESLint config.

🔧 Proposed fix
-  }, [initialGroups]);
+  }, [initialGroups]); // eslint-disable-line react-hooks/exhaustive-deps
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
React.useEffect(() => {
if (initialGroups) {
setPeerExposeGroups(initialGroups);
const groupIds = initialGroups
.map((g) => g.id)
.filter(Boolean) as string[];
updateRef([
autoUpdateMethod,
autoUpdateCustomVersion,
peerExposeEnabled,
groupIds,
]);
}
}, [initialGroups]);
React.useEffect(() => {
if (initialGroups) {
setPeerExposeGroups(initialGroups);
const groupIds = initialGroups
.map((g) => g.id)
.filter(Boolean) as string[];
updateRef([
autoUpdateMethod,
autoUpdateCustomVersion,
peerExposeEnabled,
groupIds,
]);
}
}, [initialGroups]); // eslint-disable-line react-hooks/exhaustive-deps
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 100 - 113, The
useEffect that reads initialGroups and calls setPeerExposeGroups/updateRef
references autoUpdateMethod, autoUpdateCustomVersion, peerExposeEnabled, and
updateRef but intentionally omits them from the dependency array; add an eslint
suppression comment to avoid react-hooks/exhaustive-deps warnings: place a
single-line comment like // eslint-disable-next-line react-hooks/exhaustive-deps
immediately above the React.useEffect(...) (which uses initialGroups,
setPeerExposeGroups, and updateRef) so the linter knows the omission is
intentional.

Comment on lines 338 to 367
<AnimatePresence>
{peerExposeEnabled && (
<div className={"overflow-hidden -top-4 relative z-0"}>
<motion.div
initial={{ opacity: 0, height: 0, scale: 0.98 }}
animate={{ opacity: 1, height: "auto", scale: 1 }}
exit={{ opacity: 0, height: 0, scale: 0.98 }}
>
<div
className={
"flex flex-col gap-6 bg-nb-gray-940 px-6 pt-5 pb-6 border border-nb-gray-930 rounded-b-md relative mx-3"
}
>
<div>
<Label>Allowed peer groups</Label>
<HelpText>
Restrict which peer groups are allowed to expose
services. Leave empty to allow all peers.
</HelpText>
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
/>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Framer Motion AnimatePresence key prop required direct children documentation

💡 Result:

Framer Motion’s AnimatePresence docs explicitly state that its immediate (direct) children must each have a unique React key prop so AnimatePresence can track whether each child is entering/exiting the React tree. (motion.dev)

The same page reiterates this in Troubleshooting → “Exit animations aren’t working”: ensure all immediate children have a unique, stable key (and avoid using array indexes as keys if items can reorder). (motion.dev)

Citations:


🏁 Script executed:

# Find and examine the ClientSettingsTab.tsx file
find . -name "ClientSettingsTab.tsx" -type f

Repository: netbirdio/dashboard

Length of output: 107


🏁 Script executed:

# Read the ClientSettingsTab.tsx file around lines 338-367
sed -n '335,370p' ./src/modules/settings/ClientSettingsTab.tsx

Repository: netbirdio/dashboard

Length of output: 1543


🏁 Script executed:

# Verify the broader usage context - check if there are similar AnimatePresence patterns
# to understand if this is a pattern issue across the file
rg "AnimatePresence" ./src/modules/settings/ClientSettingsTab.tsx -A 5

Repository: netbirdio/dashboard

Length of output: 588


Add key prop to the direct child of AnimatePresence.

Framer Motion's official documentation explicitly requires that all immediate (direct) children of AnimatePresence must have a unique React key prop so AnimatePresence can track whether each child is entering or exiting the React tree. The wrapper <div> at line 340 is missing this required key. While the exit animation on the nested motion.div may work in practice due to context propagation, omitting the key violates the documented API contract and is not guaranteed to work reliably across different scenarios.

✨ Proposed fix
-          <div className={"overflow-hidden -top-4 relative z-0"}>
+          <div key="peer-expose-groups" className={"overflow-hidden -top-4 relative z-0"}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<AnimatePresence>
{peerExposeEnabled && (
<div className={"overflow-hidden -top-4 relative z-0"}>
<motion.div
initial={{ opacity: 0, height: 0, scale: 0.98 }}
animate={{ opacity: 1, height: "auto", scale: 1 }}
exit={{ opacity: 0, height: 0, scale: 0.98 }}
>
<div
className={
"flex flex-col gap-6 bg-nb-gray-940 px-6 pt-5 pb-6 border border-nb-gray-930 rounded-b-md relative mx-3"
}
>
<div>
<Label>Allowed peer groups</Label>
<HelpText>
Restrict which peer groups are allowed to expose
services. Leave empty to allow all peers.
</HelpText>
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
/>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<AnimatePresence>
{peerExposeEnabled && (
<div key="peer-expose-groups" className={"overflow-hidden -top-4 relative z-0"}>
<motion.div
initial={{ opacity: 0, height: 0, scale: 0.98 }}
animate={{ opacity: 1, height: "auto", scale: 1 }}
exit={{ opacity: 0, height: 0, scale: 0.98 }}
>
<div
className={
"flex flex-col gap-6 bg-nb-gray-940 px-6 pt-5 pb-6 border border-nb-gray-930 rounded-b-md relative mx-3"
}
>
<div>
<Label>Allowed peer groups</Label>
<HelpText>
Restrict which peer groups are allowed to expose
services. Leave empty to allow all peers.
</HelpText>
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
/>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 338 - 367, The
direct child of AnimatePresence (the outer wrapper div rendered when
peerExposeEnabled is true) must include a unique React key so Framer Motion can
track enter/exit; update the JSX that renders the wrapper div (the element
containing className "overflow-hidden -top-4 relative z-0") to include a stable
key (e.g. key="peer-expose" or derived from peerExposeEnabled) while leaving the
inner motion.div and PeerGroupSelector, setPeerExposeGroups, and
peerExposeGroups logic unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 364-368: PeerGroupSelector is interactive for users without update
rights; add a disabled guard by passing disabled={!permission.settings.update}
to the PeerGroupSelector and prevent updates when disabled (e.g., make onChange
a no-op or conditionally call setPeerExposeGroups only if
permission.settings.update is true) so the selector is visibly and functionally
read-only for users lacking permission.

---

Duplicate comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 100-113: The effect that runs on initialGroups is calling
updateRef with live state variables (autoUpdateMethod, autoUpdateCustomVersion,
peerExposeEnabled) causing a stale-closure reset; change the effect to read the
stable initial/saved values from the account/settings snapshot (the mount-time
values) instead of those live variables (e.g., capture initialAutoUpdateMethod,
initialAutoUpdateCustomVersion, initialPeerExposeEnabled from the account
snapshot used on mount) and pass those into updateRef along with groupIds, then
keep setPeerExposeGroups(initialGroups) as-is; finally, add the required //
eslint-disable-line react-hooks/exhaustive-deps comment to the useEffect to
silence the lint rule once only the stable snapshot values are used.
- Around line 345-374: AnimatePresence requires its direct children to have
unique key props so exit animations run; add a stable key to the wrapper div
(the direct child of AnimatePresence) that wraps the motion.div (for example use
key="peer-expose" or a value derived from peerExposeEnabled) or alternatively
make the motion.div the direct child and give it a key; update the wrapper/div
or motion.div inside ClientSettingsTab (the block rendering when
peerExposeEnabled is true) so the direct child of AnimatePresence has a unique
key to ensure the exit animation defined on the motion.div runs.

Comment on lines +364 to +368
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the PeerGroupSelector component definition
fd "PeerGroupSelector" --type ts --type tsx

Repository: netbirdio/dashboard

Length of output: 232


🏁 Script executed:

# Search for PeerGroupSelector in the codebase
rg "PeerGroupSelector" --type ts --type tsx -l

Repository: netbirdio/dashboard

Length of output: 90


🏁 Script executed:

# Examine the ClientSettingsTab.tsx file around lines 364-368
rg -n "PeerGroupSelector" src/modules/settings/ClientSettingsTab.tsx -A 5 -B 5

Repository: netbirdio/dashboard

Length of output: 1096


🏁 Script executed:

# Find the PeerGroupSelector component file
find . -name "*PeerGroupSelector*" -type f

Repository: netbirdio/dashboard

Length of output: 101


🏁 Script executed:

# Search for PeerGroupSelector component definition
rg "export.*PeerGroupSelector\|function PeerGroupSelector\|const PeerGroupSelector" -A 20

Repository: netbirdio/dashboard

Length of output: 45


🏁 Script executed:

# Check FancyToggleSwitch at line 342 in ClientSettingsTab.tsx
sed -n '340,345p' src/modules/settings/ClientSettingsTab.tsx

Repository: netbirdio/dashboard

Length of output: 265


🏁 Script executed:

# Read the PeerGroupSelector component file
cat -n src/components/PeerGroupSelector.tsx

Repository: netbirdio/dashboard

Length of output: 39132


PeerGroupSelector is interactive for read-only users — missing disabled guard.

FancyToggleSwitch correctly uses disabled={!permission.settings.update} (line 342), but PeerGroupSelector (lines 364–368) has no equivalent guard. Users lacking settings.update permission can still interact with the selector; the Save button prevents persistence, but the interactive UI is misleading.

Proposed fix
           <PeerGroupSelector
             values={peerExposeGroups}
             onChange={setPeerExposeGroups}
             placeholder="Select peer groups..."
+            disabled={!permission.settings.update}
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
/>
<PeerGroupSelector
values={peerExposeGroups}
onChange={setPeerExposeGroups}
placeholder="Select peer groups..."
disabled={!permission.settings.update}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 364 - 368,
PeerGroupSelector is interactive for users without update rights; add a disabled
guard by passing disabled={!permission.settings.update} to the PeerGroupSelector
and prevent updates when disabled (e.g., make onChange a no-op or conditionally
call setPeerExposeGroups only if permission.settings.update is true) so the
selector is visibly and functionally read-only for users lacking permission.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/modules/settings/ClientSettingsTab.tsx (1)

182-202: toggleLazyConnection spreads stale account.settings, risking overwrite of concurrently saved fields.

toggleLazyConnection sends { ...account.settings, lazy_connection_enabled: toggle } — if the account prop hasn't been refreshed by SWR after a prior "Save Changes" (which updates auto_update_version and extra), those fields will be overwritten with their previous values. With the new extra.peer_expose_* fields this surface has grown.

This is a pre-existing pattern, so no immediate fix is required, but worth noting: a safer approach would be to PATCH only the changed field or read from the SWR cache directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 182 - 202,
toggleLazyConnection currently spreads the potentially stale account.settings
into saveRequest.put which can overwrite concurrently-updated fields; change it
to only send the minimal patch (e.g., { lazy_connection_enabled: toggle }) or
else read the latest account data from the SWR cache before merging so you don't
clobber fields like auto_update_version or extra.*; update the call sites in
toggleLazyConnection (saveRequest.put) and keep setLazyConnection and
mutate("/accounts") behavior the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 100-113: The useEffect that runs on initialGroups intentionally
omits dependencies autoUpdateMethod, autoUpdateCustomVersion, peerExposeEnabled
and updateRef which trips react-hooks/exhaustive-deps; add an
eslint-disable-next-line comment for react-hooks/exhaustive-deps immediately
above this useEffect (the block containing useEffect(() => { if (initialGroups)
{ setPeerExposeGroups(initialGroups); ... updateRef([...]) } },
[initialGroups]);) so the linter knows the omission is intentional while keeping
the current dependency array.
- Around line 348-377: The AnimatePresence immediate child div rendered when
peerExposeEnabled is true needs a stable unique key (e.g., use
key={peerExposeEnabled ? 'peer-expose-panel' : 'peer-expose-panel-hidden'} or a
constant string) so Framer Motion can track enter/exit; also prevent editing by
read-only users by passing a disabled prop to PeerGroupSelector (or
conditionally render a non-interactive view) using permission.settings.update
(e.g., disabled={!permission.settings.update}) so PeerGroupSelector
(values={peerExposeGroups}, onChange={setPeerExposeGroups}) is not interactive
for users without update permission.

---

Nitpick comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 182-202: toggleLazyConnection currently spreads the potentially
stale account.settings into saveRequest.put which can overwrite
concurrently-updated fields; change it to only send the minimal patch (e.g., {
lazy_connection_enabled: toggle }) or else read the latest account data from the
SWR cache before merging so you don't clobber fields like auto_update_version or
extra.*; update the call sites in toggleLazyConnection (saveRequest.put) and
keep setLazyConnection and mutate("/accounts") behavior the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant