Skip to content

Tailscale panel: group machines into My/Tagged/Other and add "/" search - #10511

Open
ssandys wants to merge 6 commits into
omacom:quattrofrom
ssandys:tailscale-device-grouping-and-search
Open

Tailscale panel: group machines into My/Tagged/Other and add "/" search#10511
ssandys wants to merge 6 commits into
omacom:quattrofrom
ssandys:tailscale-device-grouping-and-search

Conversation

@ssandys

@ssandys ssandys commented Sep 6, 2026

Copy link
Copy Markdown

Fixes #10509

Stacked on #10510. Both branches touch the same region of Panel.qml and append to the same heredoc in tailscale-test.sh, so this cannot branch off quattro independently without a guaranteed conflict. Until #10510 merges, this PR shows its two commits as well; the last four are the ones under review here. Happy to rebase once #10510 lands, or to squash them together if you'd prefer one PR.

The MACHINES list rendered every online peer in one flat alphabetical list. On the tailnet this was developed against that is 135 rows, of which exactly one is a machine belonging to the user — the rest are tagged infrastructure and colleagues' devices.

Both changes match tsui behaviour.

Grouping

Splits the list into My Devices / Tagged Devices / Other Devices. Tagged wins over ownership, matching tsui: a tagged device is owned by the tag, not by whoever enrolled it, so atl-exit-node sits under Tagged Devices. Empty groups hide themselves, so a personal tailnet with no tagged devices shows no stray heading.

tsui uses only My Devices and Tagged Devices, but peers belonging to other users fall outside both, so a third group keeps everything reachable rather than silently hiding rows the panel shows today.

No new data plumbing — peerFromStatus already normalizes UserID and Tags, and parseStatus already exposes selfUserId.

Cursor bounds move from tailscale.peers to a new orderedPeers, the flat concatenation of the three groups in render order, with a per-group groupOffset so rowIndex stays unique across boundaries. Without this, keyboard selection would highlight a different row than the cursor is on.

Search

/ opens a search field filtering on name, MagicDNS name, OS and IP. Filtering runs before grouping, so empty groups collapse instead of leaving three headings over an empty list.

Matching the MagicDNS name matters: hostname and DNS name diverge (see #10508), so a hostname-only search would miss the name the user knows the machine by.

Also fixes a pre-existing bug

PanelKeyCatcher uses Keys.priority: Keys.BeforeItem and consumes h/j/k/l/x as navigation. Its header documents that a panel with an inline editor must set blocked: <editor>.activeFocus. Panel.qml set only blocked: root.copyMenuOpen, so the existing Mullvad region search field never had this guard — typing j/k/h/l/x into it moved the panel cursor instead of entering text. Both fields are covered now.

I have no Mullvad exit nodes on my tailnet, so that half is correct by the component's documented contract but unverified at runtime.

Tests

24 new assertions in test/shell.d/tailscale-test.sh covering grouping and filtering, including tagged-beats-ownership, order preservation and degenerate input.

Verified on a live tailnet: groups render with correct membership and counts summing to tailscale.peers.length; / opens search; typing alex enters as literal text (chosen because l and x are navigation keys — it exercises the blocked fix); no-match shows an empty state; Escape closes search without closing the panel.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK

ssandys and others added 6 commits September 6, 2026 12:23
Exit node rows name an identifier, not a friendly label: the value shown
must match `tailscale exit-node list`, tsui and the admin console so it
can be cross-referenced and typed into `tailscale set --exit-node=`.

displayHostName is deliberately left alone -- it prefers the OS hostname,
which reads better in the MACHINES list, and its localhost fallback is
load-bearing for iOS peers that all report "localhost".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK
The panel labelled an exit node with its OS hostname, so a machine whose
HostName is "Firezone" and DNSName is "ny-exit-node.tailnet.ts.net" showed
as "Firezone" -- disagreeing with `tailscale exit-node list`, tsui and the
admin console, which all call it "ny-exit-node".

Cosmetic only: exitNodeTarget already resolves through peerAddress, which
prefers DNSName, so the correct target was always being set.

MACHINES rows are deliberately unchanged, with a source guard asserting so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK
Adds peerGroup/groupPeers so the machine list can be split into My
Devices, Tagged Devices and Other Devices.

Tagged wins over ownership, matching tsui: a tagged device is owned by
the tag, not by whoever enrolled it, so atl-exit-node belongs under
Tagged Devices rather than My Devices.

No new data plumbing -- peerFromStatus already normalizes UserID and
Tags, and parseStatus already exposes selfUserId.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK
Renders MACHINES as three headed groups instead of one flat alphabetical
list. Empty groups hide themselves, so a personal tailnet with no tagged
devices shows no stray heading.

Cursor bounds move from tailscale.peers to a new orderedPeers, the flat
concatenation of the three groups in render order, and each group applies
a groupOffset so rowIndex stays unique across group boundaries. Without
this, keyboard selection would highlight a different row than the one the
cursor is on -- and orderedPeers.length will diverge from
tailscale.peers.length once search filtering lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK
peerMatchesQuery searches DisplayName, HostName, DNSName, OS and every
Tailscale IP. Matching the MagicDNS name matters: hostname and DNS name
diverge (a machine named "Firezone" answers to ny-exit-node), so a
hostname-only search would miss the name the user knows it by.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK
Press / in the panel to filter machines by name, MagicDNS name or IP.
Filtering runs before grouping, so empty groups hide themselves instead
of leaving three headings over an empty list.

Also fixes the blocked binding on PanelKeyCatcher, which only covered the
copy menu. PanelKeyCatcher uses Keys.priority: Keys.BeforeItem and
consumes h/j/k/l/x as navigation, so its header documents that a panel
with an inline editor must set blocked: <editor>.activeFocus. That was
never done for the Mullvad region search, so typing j/k/h/l/x into it
moved the panel cursor instead of entering text. Both fields are covered
now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwrnwejcfKriS8iBcaTcmK
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.

Tailscale panel: group machines into My/Tagged/Other and add "/" search

1 participant