Skip to content

Restore combined Peers page with server and device filters#668

Open
ramphex wants to merge 1 commit into
netbirdio:mainfrom
ramphex:fix/peers-kind
Open

Restore combined Peers page with server and device filters#668
ramphex wants to merge 1 commit into
netbirdio:mainfrom
ramphex:fix/peers-kind

Conversation

@ramphex

@ramphex ramphex commented Jun 15, 2026

Copy link
Copy Markdown

Issue ticket number and link

Closes #663

Issue #663 reports that Dashboard v2.39.0 reduced peer-list usability by splitting the previous unified Peers view into two separate navigation destinations: User Devices and Servers. That split made it impossible to see all peers in one sortable/searchable/filterable table, forcing users to move back and forth between two pages when they needed a complete view of their network.

This PR restores /peers as the canonical combined Peers page while preserving the ability to focus the list by peer type. Instead of separate sidebar destinations, the Peers page now shows both Servers and Devices in one table by default and provides inline Servers / Devices toggles for quick visibility filtering. This directly addresses the issue request: “go back to a full list with ‘User Devices’ and ‘Servers’ toggles to filter visibility instead.”

The previous classification logic is still useful as a default because peers enrolled with setup keys are likely servers most of the time, and peers enrolled through SSO are likely user devices most of the time. However, that assumption is not always correct. Some setup-key peers are regular devices, and some SSO-enrolled peers may function as servers or shared infrastructure. Treating enrollment method as the only source of truth makes the experience clunky, especially once users need to organize or audit mixed environments from the Peers page. This PR keeps that inference as the automatic fallback, but prepares the dashboard to respect an explicit backend-provided peer type when available.

The change also keeps the combined view practical:

  • The sidebar Peers item now links directly to /peers instead of opening only the old split page dropdown.
  • The /peers page shows one combined list of all peers by default.
  • Servers and Devices can be toggled independently without leaving the page.
  • The toggle state is persisted locally, so users do not lose their preferred view when navigating away and returning.
  • The heading count updates based on the active view, for example X Peers, X Server Peers, or X Device Peers.
  • A small info tooltip next to the Peers heading preserves the explanatory text from the previous split sections, with Servers and Devices described in the same order as the toggles.
  • When both peer types are visible, each peer row can show a subtle server/device icon so users can still distinguish peer type in the combined table.
  • Peer detail breadcrumbs and Cancel navigation now return to /peers, matching the restored combined page instead of sending users back into the old split routes.

This PR also includes dashboard support for an optional backend-provided peer classification field. When the management API exposes peer.kind, the peer detail page can show/edit the peer type. When connected to an older backend that does not return that field, the editor is hidden and the dashboard falls back to the existing inference logic. That keeps the dashboard backward-compatible while allowing newer backends to support explicit peer classification.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

This change restores and improves the dashboard Peers list behavior requested in issue #663. The relevant guidance is already presented in the UI through the Peers heading tooltip, which preserves the explanatory Servers and Devices wording from the previously split sections. No external documentation page currently describes the split Peers navigation or the new table toggles, and this PR does not require users to follow a new setup procedure.

Summary by CodeRabbit

  • New Features

    • Added peer type support (device vs. server) to peer information and dashboard views
    • Introduced optional device/server filtering in the peers list
    • Added visual indicators for peer types in the peers table
  • UI/UX Improvements

    • Simplified navigation by removing nested peer categories
    • Enhanced peer dashboard with type selection and management
    • Improved peers list organization with better conditional rendering and tooltips

@CLAassistant

CLAassistant commented Jun 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a new peerKind.ts domain module with types and helpers for peer kind resolution. The Peer interface and PeerProvider gain optional kind field support. PeersTable adds a toggleable device/server kind filter with localStorage persistence and a ButtonGroup UI. The /peers route becomes a full client page. The sidebar "Peers" entry is flattened. The peer detail page adds kind editing.

Changes

Peer Kind Feature End-to-End

Layer / File(s) Summary
peerKind domain types, labels, and helpers
src/modules/peers/peerKind.ts
New module defines PeerKind, ResolvedPeerKind, PeersTableKind types, label maps (PEER_KIND_LABELS, PEERS_TABLE_KIND_LABELS), and helper functions: supportsPeerKind, normalizePeerKind, inferPeerKind, getEffectivePeerKind, matchesPeerTableKind.
Peer interface and PeerProvider kind support
src/interfaces/Peer.ts, src/contexts/PeerProvider.tsx
Peer interface gains optional kind?: PeerKind; PeerProvider extends the update signature with optional kind and conditionally includes it in the API payload.
ActiveInactiveRow textPrefix and PeerNameCell kind icon
src/modules/common-table-rows/ActiveInactiveRow.tsx, src/modules/peers/PeerNameCell.tsx
ActiveInactiveRow gains optional textPrefix rendered before existing content; PeerNameCell adds showPeerKindIcon prop and a PeerKindIndicator subcomponent with icon and tooltip.
DataTable heading count label support
src/components/table/DataTable.tsx, src/components/table/DataTableHeadingPortal.tsx
DataTableProps gains headingCountLabel; DataTable forwards it to DataTableHeadingPortal as countLabel. Portal DOM management is refactored to useRef/useEffect, adds showHeadingCount gating, and Heading formatting includes the label.
PeersTable kind filter, toggle UI, and column factory
src/modules/peers/PeersTable.tsx
Adds showKindFilters prop, DEFAULT_ENABLED_KINDS, localStorage-backed enabledKinds state, matchesPeerTableKind filtering, a ButtonGroup toggle UI, dynamic column factory getPeersTableColumns, NoResults empty state for zero kind-filter results, and wires headingCountLabel and peersTableColumns into DataTable.
/peers route: full client page with blocked/normal views
src/app/(dashboard)/peers/page.tsx
Replaces the redirect with a client PeersPage that renders PeersBlockedView (with SetupModalContent) or a PeersProvider-wrapped PeersView that lazily loads PeersTable inside Suspense.
Peer detail: kind editing UI and navigation cleanup
src/app/(dashboard)/peer/page.tsx
Removes legacy list-path routing; fixes breadcrumb and Cancel navigation to /peers; adds handleSavePeerKind (calls update({kind}) and SWR revalidation); conditionally renders a Peer Type row with PeerKindSelect or PeerKindValue components.
Sidebar Peers entry flattened
src/layouts/Navigation.tsx
Removes collapsible Peers group with nested User Devices/Servers items; replaces with a single SidebarItem visible when !isRestricted.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PeersPage
    participant PeersProvider
    participant PeersTable
    participant peerKind

    User->>PeersPage: navigate to /peers
    PeersPage->>PeersPage: check isRestricted via usePermissions()
    alt is restricted
        PeersPage->>User: render PeersBlockedView + SetupModalContent
    else normal
        PeersPage->>PeersProvider: wrap PeersView
        PeersProvider->>PeersTable: provide peers + users context
        PeersTable->>peerKind: matchesPeerTableKind(peer, enabledKind)
        peerKind-->>PeersTable: boolean match result
        PeersTable->>PeersTable: filter kindFilteredPeers, compute headingCountLabel
        User->>PeersTable: toggle kind ButtonGroup
        PeersTable->>PeersTable: setEnabledKinds, reset selection, jump to page 0
        PeersTable->>User: render filtered rows + count label
    end
Loading
sequenceDiagram
    participant User
    participant PeerDetailPage
    participant PeerProvider
    participant SWR

    User->>PeerDetailPage: select peer kind via PeerKindSelect
    PeerDetailPage->>PeerDetailPage: derive hasPeerKind, selectedPeerKind, inferredPeerKind
    User->>PeerDetailPage: save peer kind
    PeerDetailPage->>PeerProvider: update({ kind })
    PeerProvider->>PeerProvider: build payload, conditionally add kind
    PeerProvider-->>PeerDetailPage: updated Peer
    PeerDetailPage->>SWR: mutate /peers/{id} and /peers
    SWR-->>PeerDetailPage: revalidated data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • netbirdio/dashboard#649: Directly overlaps with the peers UI refactor in the same route (/app/(dashboard)/peers/page.tsx), Navigation "Peers" entry simplification, and PeersTable kind/type filtering changes.
  • netbirdio/dashboard#594: Both PRs modify src/contexts/PeerProvider.tsx to extend the update function with a new optional peer field (kind here, ipv6 there), sharing the same payload-construction pattern.

Suggested reviewers

  • heisbrot
  • mlsmaycon

Poem

🐇 Hoppity-hop, the peers now stand tall,
One unified list, no more walls!
Device or server? A click sorts it out,
The sidebar grew slim — less clutter, no doubt.
Kind filters persist through localStorage's gate,
This bunny approves — the UX is great! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the PR: restoring the combined Peers page with integrated server and device filters.
Description check ✅ Passed The PR description comprehensively covers the changes, including issue reference, detailed explanations of the restored functionality, and documentation justification.
Linked Issues check ✅ Passed The code changes fully address issue #663's request: restoring a combined peers page with Servers/Devices toggles for filtering instead of separate navigation routes.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to implementing the combined peers page, filtering UI, peer-kind field support, and related navigation updates requested in issue #663.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed due to a network error.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/app/(dashboard)/peers/page.tsx (1)

43-49: ⚡ Quick win

Reduce join complexity in peers-to-users mapping.

At Line 45-Line 48, each peer does a linear users.find(...), making this O(peers * users). Build a user-id map once and do O(1) lookups to keep table rendering stable on larger accounts.

Suggested refactor
   const peersWithUser = useMemo(() => {
     if (!peers || !users) return undefined;
+    const usersById = new Map(users.map((u) => [u.id, u]));
     return peers.map((peer) => ({
       ...peer,
-      user: users.find((u) => u.id === peer.user_id),
+      user: usersById.get(peer.user_id),
     }));
   }, [peers, users]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/peers/page.tsx around lines 43 - 49, The peersWithUser
useMemo is inefficient because it performs a linear search using users.find()
for each peer, resulting in O(peers * users) complexity. Fix this by first
creating a Map keyed by user ID from the users array before the peers.map()
operation, then replace the users.find() call with a direct O(1) Map lookup to
maintain stable table rendering performance on larger datasets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(dashboard)/peer/page.tsx:
- Around line 641-665: The Card.ListItem for "Peer Type" is currently hidden
entirely when hasPeerKind is false, but it should instead remain visible to show
the inferred peer type in read-only mode for older backends. Remove the
conditional wrapper {hasPeerKind && (...)} around the Card.ListItem so the row
is always rendered. The conditional logic inside the value prop (which shows
PeerKindSelect when permission.peers.update is true and PeerKindValue otherwise)
will properly handle showing the read-only inferred type for older backends that
don't support peer kind updates.

In `@src/app/`(dashboard)/peers/page.tsx:
- Around line 100-103: The InlineLink component instances that use
target="_blank" are missing the rel="noopener noreferrer" attribute, which
exposes the application to reverse-tabnabbing attacks. Add the rel="noopener
noreferrer" attribute to each InlineLink component where target="_blank" is
specified (there are multiple instances in the peers/page.tsx file). This
prevents the opened page from accessing the window.opener property and
potentially redirecting the original page.

---

Nitpick comments:
In `@src/app/`(dashboard)/peers/page.tsx:
- Around line 43-49: The peersWithUser useMemo is inefficient because it
performs a linear search using users.find() for each peer, resulting in O(peers
* users) complexity. Fix this by first creating a Map keyed by user ID from the
users array before the peers.map() operation, then replace the users.find() call
with a direct O(1) Map lookup to maintain stable table rendering performance on
larger datasets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2da1355-5c64-4975-a150-25321c19c7b1

📥 Commits

Reviewing files that changed from the base of the PR and between 5eac516 and 8bc3a13.

📒 Files selected for processing (11)
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(dashboard)/peers/page.tsx
  • src/components/table/DataTable.tsx
  • src/components/table/DataTableHeadingPortal.tsx
  • src/contexts/PeerProvider.tsx
  • src/interfaces/Peer.ts
  • src/layouts/Navigation.tsx
  • src/modules/common-table-rows/ActiveInactiveRow.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/peers/PeersTable.tsx
  • src/modules/peers/peerKind.ts

Comment on lines +641 to +665
{hasPeerKind && (
<Card.ListItem
tooltip={false}
label={
<>
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
Peer Type
</>
}
value={
permission.peers.update ? (
<PeerKindSelect
value={selectedPeerKind}
inferredKind={inferredPeerKind}
onChange={handleSavePeerKind}
/>
) : (
<PeerKindValue
value={selectedPeerKind}
inferredKind={inferredPeerKind}
/>
)
}
/>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Show inferred peer type on older backends instead of hiding the entire row.

This block hides “Peer Type” when supportsPeerKind(peer) is false. That drops fallback visibility for older backends; the editor should be hidden there, but inferred type should still be shown read-only.

Proposed fix
-          {hasPeerKind && (
-            <Card.ListItem
-              tooltip={false}
-              label={
-                <>
-                  <MonitorSmartphoneIcon size={16} className={"shrink-0"} />
-                  Peer Type
-                </>
-              }
-              value={
-                permission.peers.update ? (
-                  <PeerKindSelect
-                    value={selectedPeerKind}
-                    inferredKind={inferredPeerKind}
-                    onChange={handleSavePeerKind}
-                  />
-                ) : (
-                  <PeerKindValue
-                    value={selectedPeerKind}
-                    inferredKind={inferredPeerKind}
-                  />
-                )
-              }
-            />
-          )}
+          <Card.ListItem
+            tooltip={false}
+            label={
+              <>
+                <MonitorSmartphoneIcon size={16} className={"shrink-0"} />
+                Peer Type
+              </>
+            }
+            value={
+              hasPeerKind && permission.peers.update ? (
+                <PeerKindSelect
+                  value={selectedPeerKind}
+                  inferredKind={inferredPeerKind}
+                  onChange={handleSavePeerKind}
+                />
+              ) : (
+                <PeerKindValue
+                  value={selectedPeerKind}
+                  inferredKind={inferredPeerKind}
+                />
+              )
+            }
+          />
📝 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
{hasPeerKind && (
<Card.ListItem
tooltip={false}
label={
<>
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
Peer Type
</>
}
value={
permission.peers.update ? (
<PeerKindSelect
value={selectedPeerKind}
inferredKind={inferredPeerKind}
onChange={handleSavePeerKind}
/>
) : (
<PeerKindValue
value={selectedPeerKind}
inferredKind={inferredPeerKind}
/>
)
}
/>
)}
<Card.ListItem
tooltip={false}
label={
<>
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
Peer Type
</>
}
value={
hasPeerKind && permission.peers.update ? (
<PeerKindSelect
value={selectedPeerKind}
inferredKind={inferredPeerKind}
onChange={handleSavePeerKind}
/>
) : (
<PeerKindValue
value={selectedPeerKind}
inferredKind={inferredPeerKind}
/>
)
}
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/peer/page.tsx around lines 641 - 665, The Card.ListItem
for "Peer Type" is currently hidden entirely when hasPeerKind is false, but it
should instead remain visible to show the inferred peer type in read-only mode
for older backends. Remove the conditional wrapper {hasPeerKind && (...)} around
the Card.ListItem so the row is always rendered. The conditional logic inside
the value prop (which shows PeerKindSelect when permission.peers.update is true
and PeerKindValue otherwise) will properly handle showing the read-only inferred
type for older backends that don't support peer kind updates.

Comment on lines +100 to +103
<InlineLink
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
target={"_blank"}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate InlineLink implementation
fd -i 'InlineLink' src

# Inspect component behavior around target/rel propagation
rg -n -C4 'target|rel|noopener|noreferrer' src --iglob '*InlineLink*'

Repository: netbirdio/dashboard

Length of output: 1154


🏁 Script executed:

cat -n src/components/InlineLink.tsx

Repository: netbirdio/dashboard

Length of output: 2035


🏁 Script executed:

sed -n '95,140p' src/app/\(dashboard\)/peers/page.tsx

Repository: netbirdio/dashboard

Length of output: 1527


Add rel="noopener noreferrer" to protect against reverse-tabnabbing.

The InlineLink component does not automatically set the rel attribute when target="_blank" is used. Both instances at lines 100–103 and 130–133 in peers/page.tsx need explicit rel="noopener noreferrer" to prevent reverse-tabnabbing attacks.

Either:

  1. Update the InlineLink component to automatically include rel="noopener noreferrer" when target="_blank" is present, or
  2. Add rel="noopener noreferrer" directly to each InlineLink that uses target="_blank".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/peers/page.tsx around lines 100 - 103, The InlineLink
component instances that use target="_blank" are missing the rel="noopener
noreferrer" attribute, which exposes the application to reverse-tabnabbing
attacks. Add the rel="noopener noreferrer" attribute to each InlineLink
component where target="_blank" is specified (there are multiple instances in
the peers/page.tsx file). This prevents the opened page from accessing the
window.opener property and potentially redirecting the original page.

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.

New Peers section reduced functionality (v2.39.0)

2 participants