Skip to content

feat: host folder grouping for all host types#5358

Open
tomsihap wants to merge 8 commits into
NginxProxyManager:developfrom
tomsihap:develop
Open

feat: host folder grouping for all host types#5358
tomsihap wants to merge 8 commits into
NginxProxyManager:developfrom
tomsihap:develop

Conversation

@tomsihap
Copy link
Copy Markdown

Summary

  • Adds a Folder field to the Edit modal of all 4 host types (Proxy Hosts, Redirection Hosts, Streams, 404 Hosts), backed by meta.folder — no DB migration required
  • Groups hosts by folder in each dashboard table using TanStack Table v8 built-in grouping, with a collapsible folder header row showing enabled/disabled badge counts
  • Extends client-side search to match folder names in addition to existing fields
  • Hosts without a folder appear at the top of the table as a flat list; named folders appear below, collapsed by default

Technical details

  • Data model: meta.folder (string, optional) on existing meta JSON column — zero schema changes
  • Shared infrastructure: TableBody extended with a renderRow escape-hatch (backward-compatible) allowing custom grouped row rendering
  • Reusable component: FolderField combobox reads existing folder names from the React Query cache via prefix-matching (getQueriesData) for autocomplete — no extra API calls
  • TanStack grouping: hidden folder accessor column drives grouping; columnVisibility: { folder: false } keeps it out of rendered cells

Behaviour

Scenario Result
Host without folder Flat list at top (unchanged appearance)
Folder default state Collapsed
Folder persistence None (resets on page load)
Search matches host inside folder Folder header visible, stays closed
Search matches folder name Folder header visible, stays closed
Create folder Type name in Edit modal Folder field
Remove from folder Clear Folder field in Edit modal

Screenshots

image image image image

Test plan

  • Start dev stack: ./scripts/start-dev → open http://localhost:3081
  • Edit a Proxy Host → Details tab → "Folder" field visible below Access List
  • Assign two hosts to the same folder → folder accordéon appears in table
  • Click folder header → expands/collapses
  • Reload page → folders collapsed again
  • Type folder name in search → folder header appears (stays closed)
  • Type domain name in search → matching hosts found regardless of folder
  • Edit host → clear Folder field → host moves back to flat list
  • Repeat above for Redirection Hosts, Streams, 404 Hosts

🤖 Generated with Claude Code

tomsihap and others added 7 commits February 28, 2026 11:44
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows users to assign hosts to named folders for visual grouping in
the Proxy Hosts, Redirection Hosts, Streams, and 404 Hosts dashboards.

- Add Folder field (CreatableSelect) to all four host edit modals,
  stored in meta.folder — no database migration required
- Group hosts by folder in each table using TanStack Table v8 built-in
  grouping with a collapsible header row showing status counts
- Hosts without a folder appear at the top as a flat list; named
  folders appear below, collapsed by default
- Extend client-side search to match folder names alongside existing fields
- Add renderRow escape-hatch to TableBody for custom grouped row rendering
- Folder status indicators use Tabler status-lime/status-red dots,
  consistent with the existing host status column
feat: host folder grouping for all host types
@tomsihap
Copy link
Copy Markdown
Author

Feel free to close if the contributing policy refuses Claude Code. This PR aims to cover this old issue #409

@nginxproxymanagerci
Copy link
Copy Markdown

Docker Image for build 2 is available on DockerHub:

nginxproxymanager/nginx-proxy-manager-dev:pr-5358

Note

Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
This is a different docker image namespace than the official image.

Warning

Changes and additions to DNS Providers require verification by at least 2 members of the community!

@TheMazeIsAmazing
Copy link
Copy Markdown

TheMazeIsAmazing commented Mar 5, 2026

Just tried the docker image, this is a great addition!

I've a suggestion regarding the styling of these folders: is it possible to make it more distinct which hosts belong to a folder, and which do not?

Example mock-up image:

image

As you could see, baserow and beszel are attached to a test folder, and librechat isn't.

This would make it much more clear when you have a mix of hosts with / without a folder.

@ghost
Copy link
Copy Markdown

ghost commented Mar 15, 2026

When is this coming to the main branch? I love it!

@jc21
Copy link
Copy Markdown
Member

jc21 commented May 14, 2026

Code Review

Overview

The feature design is sound — using meta.folder avoids a DB migration, the renderRow escape-hatch on TableBody is backward-compatible, and reading folder names from the React Query cache for autocomplete is a nice touch. A few things worth addressing before merging:


Major — Duplication across all 4 Table components

renderLeafRow, renderRow, and the entire folder header <tr> block are copy-pasted identically into ProxyHosts/Table.tsx, RedirectionHosts/Table.tsx, Streams/Table.tsx, and DeadHosts/Table.tsx. The only difference is the generic type parameter. This should be extracted into a shared utility in src/components/Table/ — e.g. a generic createFolderRenderRow<T> factory — so any future changes (styling, accessibility, behaviour) don't need to be made in 4 places.


Minor — cloneElement on a Fragment is fragile

In TableBody.tsx:

const node = renderRow(row);
return isValidElement(node) ? cloneElement(node, { key: row.id } as any) : node;

When ungrouped hosts return <>{subRows.map(renderLeafRow)}</>, cloneElement injects key onto the Fragment — but React doesn't use Fragment keys for reconciliation the same way as element keys. It works incidentally because renderLeafRow sets key={row.id} on each <tr> directly, but the intent is non-obvious to future readers. Consider returning ReactNode[] from renderRow when multiple rows are needed, or add a comment explaining why this is safe.


Minor — useMemo for a static value

const grouping: GroupingState = useMemo(() => ["folder"], []);

This is a stable constant that never changes. A module-level constant avoids the useMemo overhead and makes the intent clearer:

const GROUPING: GroupingState = ["folder"];

Minor — Inline styles repeated 4x

The folder header row uses:

style={{ backgroundColor: "var(--tblr-bg-surface-secondary, #f6f8fb)", cursor: "pointer", userSelect: "none" }}

This appears verbatim in all 4 tables. Moving it to a CSS class (e.g. .folder-group-row) would keep styles out of JS and make theme overrides easier.


UX note — Search doesn't auto-expand matched folders

When a search term matches a host inside a collapsed folder, the folder header is visible but stays closed. The user has to manually expand to see the match. I'd suggest auto-expanding folders that contain search hits when search is non-empty — it's a small change in the expanded state initialisation and would significantly improve discoverability.


Other observations

  • The isFiltered={!!search} fix in Streams/TableWrapper.tsx (was !!filtered) is a correct incidental fix — good catch.
  • Removal of the dead _deleteId/_setDeleteIdd state (with its typo) is welcome cleanup.
  • No folder name length limit — worth adding a maxLength to the CreatableSelect input to prevent overflow in the header row.

Overall this is a well-thought-out feature. The duplication is the main thing I'd want resolved before merge — everything else is polish.

@jc21
Copy link
Copy Markdown
Member

jc21 commented May 14, 2026

This conflicts with #5452 - it would be nice if you both reach a consensus on who's got the best approach. If not I will eventually have to make that call.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants