Skip to content

feat: Dashboard groups with tabs, collapsible/bordered options, and panel organization#1972

Closed
alex-fedotyev wants to merge 8 commits intomainfrom
feat/dashboard-dnd-organization
Closed

feat: Dashboard groups with tabs, collapsible/bordered options, and panel organization#1972
alex-fedotyev wants to merge 8 commits intomainfrom
feat/dashboard-dnd-organization

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented Mar 23, 2026

Summary

Dashboard organization feature: tiles can be organized into Groups — collapsible, optionally bordered containers with smart tab support, reordering via drag handles, and cross-container tile moves.

Concepts

  • Group — A container for tiles. Always has at least 1 tab internally. Configurable:

    • Collapsible (default true) — chevron toggle, collapse state stored in URL params
    • Bordered (default true) — visual border around the group
    • Tabbed — tab bar appears automatically when a group has 2+ tabs. Tiles belong to a specific tab via tabId.
  • Container schema — No type discriminator. Containers are defined by properties (collapsible, bordered, tabs). Designed to be extensible via discriminated union if other container types are needed in the future (e.g. markdown blocks, alert rows, embeds).

Features

  • Add groups via + Add → "New Group"
  • Collapse/expand groups (chevron toggle, URL-persisted state)
  • Toggle collapsible and bordered per-group via overflow menu
  • "Collapse by Default" option in overflow menu
  • Add/rename/delete tabs within a group
  • Move tiles between groups and tabs via dropdown
  • Select multiple tiles (Shift+click) → group them (Cmd+G)
  • Reorder groups via drag handles
  • Inline rename for group headers and tab names (double-click)
  • Delete group with confirmation dialog

Implementation

  • Schema: DashboardContainerSchema in common-utils/types.ts with collapsible, bordered, tabs (optional), activeTabId
  • Component: GroupContainer — unified component (replaced separate SectionHeader)
  • Hooks: useDashboardContainers (container CRUD + tab lifecycle), useTileSelection (multi-select + Cmd+G)
  • DnD: @dnd-kit for container reordering, custom DashboardDndContext

PRs

References

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 23, 2026

⚠️ No Changeset found

Latest commit: ab2945e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Mar 24, 2026 3:57pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

Knip - Unused Code Analysis

⚪ No changes detected (231 issues on both main and PR)

What is this?

Knip finds unused files, dependencies, and exports in your codebase.
This comment compares the PR branch against main to detect regressions.

Run yarn knip locally to see full details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

PR Review

  • 9 binary PNGs committed to pr-screenshots/ → Remove from repo; upload screenshots directly to the PR description as GitHub attachments instead. Binary blobs in source history are permanent and inflate clone size.

  • ⚠️ escape hotkey in useTileSelection fires globally (useTileSelection.ts:62) → Will clear tile selection while user is typing in modals, rename inputs, or any other Escape-sensitive UI. Guard with a focus check or only register when tiles are selected (selectedTileIds.size > 0).

  • ⚠️ calculateNextTilePosition ignores new tile's height when checking row fit (tilePositioning.ts) → Algorithm finds horizontal space at row Y but doesn't verify rows [Y, Y+newH) are clear. A new tile placed at y=0 with h=5 can visually overlap tiles starting at y=2. React-grid-layout will compensate on layout change, but initial placement can cause a jarring visual push.

  • ⚠️ handleGroupSelected doesn't reposition tiles (useTileSelection.ts:35-51) → Tiles grouped via Cmd+G keep their original x/y coordinates from the full dashboard. Tiles scattered across the page (e.g. x=0,y=50 and x=12,y=2) will appear sparsely distributed inside the new section until manually rearranged. Consider calling calculateNextTilePosition per tile as they're assigned to the new container.

  • ⚠️ activeTabId persisted to server on every tab click (useDashboardContainers.tsx:273-282) → All viewers share the same active tab. The comment calls this intentional (Grafana/Kibana pattern), but this is a write on every tab interaction by any viewer. Consider if this needs debouncing or if it should be viewer-local state to avoid write amplification on busy dashboards.

  • 🔍 _handleToggleSectionDB destructured but never used (DBDashboardPage.tsx) → The _-prefix suppresses lint, but it still imports and runs the hook for a feature (server-persist toggle) that's disabled. If URL-based toggle is the intended behavior long-term, remove the unused export from useDashboardContainers.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

E2E Test Results

All tests passed • 92 passed • 3 skipped • 1012s

Status Count
✅ Passed 92
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from 9d1a5c2 to 0d2b11f Compare March 23, 2026 20:56
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch 2 times, most recently from a7afc78 to fd8799e Compare March 23, 2026 21:14
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from fd8799e to 20a628e Compare March 23, 2026 21:37
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from d65d046 to 0fb0522 Compare March 23, 2026 21:47
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from 0fb0522 to e38c98b Compare March 23, 2026 21:51
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from e38c98b to 74ee92a Compare March 23, 2026 21:56
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from 74ee92a to 4aee30f Compare March 23, 2026 22:02
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from 4aee30f to ae5981a Compare March 23, 2026 22:20
@alex-fedotyev alex-fedotyev force-pushed the feat/dashboard-dnd-organization branch from ae5981a to 82579ce Compare March 23, 2026 23:35
Add 'group' type alongside 'section'. Groups always have tabs array
(min 1 tab). 1 tab = plain group, 2+ = tab bar. Tiles get optional
tabId for tab assignment. activeTabId persisted like collapsed state
(Grafana/Kibana shared view state pattern).
DashboardDndContext (116 lines): sortable container reorder provider.
DashboardDndComponents (98 lines): EmptyContainerPlaceholder with
full-width [+ Add] button, SortableSectionWrapper.
1 tab = plain header (group identity IS the first tab). 2+ tabs =
tab bar with hover-only x, inline +, double-click rename.
stopPropagation on rename inputs to prevent Cmd+Left bubbling.
Render-prop children(activeTabId). Theme-aware borders.
Grip icon on hover. stopPropagation on rename input keyboard events.
Theme-aware var(--mantine-color-default-border).
useDashboardContainers (280 lines): section/group CRUD, tab management.
Groups created with 1 tab. Add Tab creates 2nd. Delete to 1 keeps tab.
Header rename syncs tabs[0]. Always-confirm delete. handleTabChange
intentionally persists activeTabId (documented).

useTileSelection (76 lines): Shift+click + Cmd+G grouping. Clears
tabId on grouped tiles to prevent orphaned tab assignments.
Fill-right-then-wrap tile positioning via calculateNextTilePosition
(w=8, h=10 matching original defaults). Floating drag bar on hover
(no content shift). Move dropdown: tab targets shown as pipe-separated
siblings with target in normal weight, others dimmed (Logs | Metrics).
Section empty state wired with onAddTile. Cross-container moves via
dropdown. Delete confirmation always shown. Select-and-group Cmd+G.
makeId uses larger ID space to reduce collision probability.
54 tests: group always has 1 tab, add tab creates 2nd, title syncs
from tabs[0], delete to 1 keeps tab, fill-right positioning, tabId
cleanup on grouping, section reorder, tile grouping.
Temporary — remove before merge.
@knudtty
Copy link
Copy Markdown
Contributor

knudtty commented Mar 27, 2026

Hey, sorry not going to get to this today. I'll follow up after the weekend

@MikeShi42
Copy link
Copy Markdown
Contributor

@alex-fedotyev one thing that wasn't clear to me when checking out is what the main difference between groups and sections are. I understand sections can collapse, and groups can't. But is there a reason why we don't have collapsible groups (and then no longer need sections?)

@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

one thing that wasn't clear to me when checking out is what the main difference between groups and sections are. I understand sections can collapse, and groups can't. But is there a reason why we don't have collapsible groups (and then no longer need sections?)

@MikeShi42 - Great question — I've been thinking through this and I see two real options:

Option A: Two types, better names

Rename "Section" → "Row". Keep "Group" as the bordered container with optional tabs.

  • Row — lightweight, collapsible header, no border. Purely organizational, like an <h2> that owns everything below it until the next row.
  • Group — bordered container with optional tabs. Not collapsible — its value is showing content, either directly or through tabs.

Benefits:

  • Each concept has a distinct visual identity and a clear verb — rows collapse, groups contain
  • "Row" is the standard term most dashboarding tools use for this concept — users coming from other platforms will recognize it immediately
  • Lightweight rows keep dashboards visually clean when you just want to organize, not contain
  • Tabs only exist on groups, which makes sense — tabs need a visual container to belong to
  • Keeping rows and groups separate provides a cleaner path for variable-driven automation. Row repeat (duplicating a row for each variable value) is a natural fit for rows and a well-established pattern in dashboarding tools. Tab repeat (dynamically generating tabs from variable values) is a different interaction model that maps naturally to groups. Two types keeps those doors open without overloading a single concept.
  • Conditional tabs (e.g., show a tab only when a variable matches a condition) are a natural future extension for groups. When all tabs are filtered out, a group with no visible tabs simply disappears — no empty bordered box on the dashboard. Rows remain as the stable organizational structure regardless of which groups are visible.

Tradeoff: two concepts to learn, but the names make the distinction obvious.

Option B: One type, always bordered

Drop section/row entirely. Just "Group" — always bordered, always collapsible, optionally tabbed.

Benefits:

  • One concept, nothing to choose between
  • Closer to what most OOTB application monitoring vendors do (single "group" primitive)
  • Simpler schema, simpler code, simpler docs

Tradeoff: every organizational element is a bordered box, which makes dashboards visually heavier. Users who just want a collapsible label to divide their dashboard into logical areas (the most common organizational pattern in dashboarding tools) are forced into bordered containers. Variable-driven repeat would need to handle both row-style duplication and tab-style generation within a single type, making the automation model more complex. Conditional tabs that filter out all visible tabs leave an empty bordered box — either shown (looks broken) or hidden (implicit disappearance is confusing).

Stretch: A style property (bordered | minimal) could be added later to let groups render without a border. However, this effectively reintroduces two visual types under one schema type — with the added complexity that tabs on a minimal group need special handling (a tab bar without a container border looks visually unanchored).

My recommendation: Option A

The real issue isn't that there are two types — it's that "section" and "group" sound like synonyms. Renaming section to "row" makes the distinction self-evident. And keeping a lightweight option matters — not every dashboard needs boxes, but almost every large dashboard needs collapsible dividers.

@alex-fedotyev alex-fedotyev marked this pull request as draft April 3, 2026 16:01
@alex-fedotyev alex-fedotyev changed the title feat: Dashboard sections, groups with tabs, and panel organization feat: Dashboard groups with tabs, collapsible/bordered options, and panel organization Apr 3, 2026
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.

3 participants