feat: Collapsible sections — authoring UX + DashboardContainer abstraction#1926
Conversation
Add section management to the dashboard: create, rename, delete sections, toggle default collapsed state, and move tiles between sections via a menu on the tile hover toolbar. Closes #1897
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
PR Review
|
E2E Test Results✅ All tests passed • 90 passed • 3 skipped • 991s
Tests ran across 4 shards in parallel. |
Schema changes: - DashboardSection → DashboardContainer with `type` field (extensible to 'group'/'tab' later, only 'section' for now) - sectionId → containerId on tiles - sections → containers on dashboard UX improvements: - Click section title to rename directly (Kibana pattern) - Edit controls (... menu) only visible on hover - "Add Section" moved to dashboard overflow menu (not equal to Add Tile) - "Move to Section" icon reordered before delete for discoverability - IconLayoutList replaces IconFolders for clearer visual meaning - "+" button on section header to add tile directly into that section
|
@claude - code review |
|
Claude finished @alex-fedotyev's task in 2m 17s —— View job Code Review
Overall this is well-structured work. The schema abstraction is clean, the UX is thoughtful, and the tests are thorough. A few issues to address before merging. 🔴 CriticalData migration missing — existing PR #1900 introduced Fix with a read-time fallback in the API layer or a one-time migration: // packages/api/src/models/dashboard.ts or the dashboard controller
containers: { type: mongoose.Schema.Types.Array, required: false },
sections: { type: mongoose.Schema.Types.Array, required: false }, // keep for migrationOr a read-time normalizer in the API response handler: // When returning a dashboard, normalize legacy field
if (!dashboard.containers && (dashboard as any).sections) {
dashboard.containers = (dashboard as any).sections;
}
Consider making it optional with a default: 🟡 Warnings
Both call
The menu label already communicates intent ("Collapse by Default" / "Expand by Default"), so the UX isn't broken — just the prop API is misleading.
const inputRef = useRef<HTMLInputElement>(null); // line 33
// ...
<Input ref={inputRef} autoFocus ... /> // line 107
Accessibility: outer The outer container sets 🔵 Minor / SuggestionsNo confirmation on "Delete Section" (
"New Section" title always hardcoded ( Multiple rapid "Add Section" calls produce multiple containers all titled "New Section". Since IDs are unique this isn't a functional bug, but a minor quality-of-life improvement would be to auto-increment: "Move to Section" menu appears even if only one section exists and tile is in it
✅ What's good
|
Addressing code review feedbackFixed (6a45d25)
Deferred
Not changing
|
e38858c to
4b471f9
Compare
pulpdrew
left a comment
There was a problem hiding this comment.
LGTM, with a couple of non-code-related suggestions
| <Menu.Item | ||
| data-testid="add-new-section-button" | ||
| leftSection={<IconLayoutList size={16} />} | ||
| onClick={handleAddSection} | ||
| > | ||
| Add Section | ||
| </Menu.Item> |
There was a problem hiding this comment.
I'll propose improving this in a separate PR!
| } | ||
| onClick={onToggle} | ||
| > | ||
| {section.collapsed ? 'Expand by Default' : 'Collapse by Default'} |
There was a problem hiding this comment.
This confuses me a bit, because it seems like this does the same thing as just expanding/collapsing the tile on the main dashboard.
IMO, expanding/collapsing should not be persisted to the dashboard UNLESS this option is used. But the existence of this option makes it more surprising that expanding/collapsing the section on the dashboard is also persisted. This seems to just make it harder to share a dashboard, if a relevant section is collapsed on page load. I think it would be nice to persist normal expand collapse states in the URL, and then fallback to the default state (saved in the DB based on this option here ^) if there is no URL state.
I do see the comment about it being intentional though.
// Intentionally persists collapsed state to the server via setDashboard
// (same pattern as tile drag/resize). This matches Grafana and Kibana
// behavior where collapsed state is saved with the dashboard for all viewers.
There was a problem hiding this comment.
Good idea, I'll craft a separate PR for this!
## Summary - Section expand/collapse is now tracked in URL query params (`collapsed`/`expanded`) instead of persisting to the DB on every chevron click - The DB-stored `collapsed` field on `DashboardContainer` becomes the default fallback — what viewers see when opening a dashboard fresh (no URL state) - Chevron click updates URL state only (per-viewer, shareable via link) - "Collapse by Default" / "Expand by Default" menu action in the section header saves to the DB (via `setDashboard`), setting the default for all viewers - `SectionHeader` now accepts separate `collapsed`/`defaultCollapsed` props and `onToggle`/`onToggleDefaultCollapsed` handlers - Adds 7 unit tests for `SectionHeader` Implements Drew's [review feedback on PR #1926](#1926 (comment)): > IMO, expanding/collapsing should not be persisted to the dashboard UNLESS this option is used. [...] I think it would be nice to persist normal expand collapse states in the URL, and then fallback to the default state (saved in the DB based on this option here) if there is no URL state. ## Demo  Shows: expanded sections → chevron click collapses first section (URL updates to `?collapsed=...`) → menu shows "Collapse by Default" (DB action, separate from view state) ## Test plan - [x] Open a dashboard with sections — collapse/expand via chevron click, verify URL updates (`?collapsed=...` / `?expanded=...`) without saving to DB - [x] Copy the URL with collapse state and open in a new tab — verify sections reflect the URL state - [x] Open the section menu and click "Collapse by Default" — verify this saves to DB (persists after page refresh without URL params) - [x] Verify "Expand by Default" / "Collapse by Default" label reflects the DB default, not current view state - [x] Run `yarn ci:unit --testPathPatterns='SectionHeader'` — all 7 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ction (#1926) ## Summary Adds the authoring experience for dashboard sections (create, rename, delete, manage tiles) and introduces a polymorphic `DashboardContainer` abstraction that future-proofs the schema for tabs and groups. Builds on #1900 (core collapsible sections mechanics). Closes #1897. ### Schema: `DashboardSection` → `DashboardContainer` - Renamed `DashboardSectionSchema` → `DashboardContainerSchema` with a new `type` field (`'section'` for now, extensible to `'group'` / `'tab'` later) - `sectionId` → `containerId` on tiles - `sections` → `containers` on dashboards - Updated across all packages: common-utils types, API Mongoose model, app types, import/export utils ### Authoring UX | Action | How | |---|---| | **Create section** | Dashboard `...` overflow menu → "Add Section" | | **Rename section** | Click the title text directly (Kibana-style inline editing) | | **Delete section** | Hover section header → `...` → Delete Section (tiles become ungrouped, not deleted) | | **Collapse/expand** | Click section header chevron | | **Toggle default state** | Hover header → `...` → Collapse/Expand by Default | | **Add tile to section** | Hover section header → `+` button opens tile editor pre-assigned to that section | | **Move tile to section** | Hover tile → grid icon → pick target section from dropdown | | **Move tile out** | Same dropdown → "(Ungrouped)" |  ### UX polish (informed by best practices research) - **Click-to-rename** — click section title text to edit inline (no menu navigation needed) - **Hover-only controls** — `...` menu and `+` button only appear on section header hover, keeping view mode clean - **"Add Section" demoted** — moved from equal-sized button to dashboard overflow menu (section creation is less frequent than tile creation) - **"Move to Section" reordered** — placed before delete button for discoverability, uses `IconLayoutList` instead of `IconFolders` ### What's NOT in this PR (follow-up work) - **Drag tiles between sections** — needs `react-dnd` custom drag layer; data model already supports it (`containerId` update) - **Reorder sections** — needs sortable list library; data model supports it (array order) - **Tabs / Groups** — new container types; just add to the `type` enum and build UIs ## Test plan - [x] 30 unit tests pass (16 existing schema/grouping + 14 new authoring operations) - [x] All 110 dashboard tests pass unchanged - [x] ESLint clean - [x] No TypeScript errors in changed files - [x] Backward compatible — dashboards without containers render exactly as before 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary - Section expand/collapse is now tracked in URL query params (`collapsed`/`expanded`) instead of persisting to the DB on every chevron click - The DB-stored `collapsed` field on `DashboardContainer` becomes the default fallback — what viewers see when opening a dashboard fresh (no URL state) - Chevron click updates URL state only (per-viewer, shareable via link) - "Collapse by Default" / "Expand by Default" menu action in the section header saves to the DB (via `setDashboard`), setting the default for all viewers - `SectionHeader` now accepts separate `collapsed`/`defaultCollapsed` props and `onToggle`/`onToggleDefaultCollapsed` handlers - Adds 7 unit tests for `SectionHeader` Implements Drew's [review feedback on PR #1926](#1926 (comment)): > IMO, expanding/collapsing should not be persisted to the dashboard UNLESS this option is used. [...] I think it would be nice to persist normal expand collapse states in the URL, and then fallback to the default state (saved in the DB based on this option here) if there is no URL state. ## Demo  Shows: expanded sections → chevron click collapses first section (URL updates to `?collapsed=...`) → menu shows "Collapse by Default" (DB action, separate from view state) ## Test plan - [x] Open a dashboard with sections — collapse/expand via chevron click, verify URL updates (`?collapsed=...` / `?expanded=...`) without saving to DB - [x] Copy the URL with collapse state and open in a new tab — verify sections reflect the URL state - [x] Open the section menu and click "Collapse by Default" — verify this saves to DB (persists after page refresh without URL params) - [x] Verify "Expand by Default" / "Collapse by Default" label reflects the DB default, not current view state - [x] Run `yarn ci:unit --testPathPatterns='SectionHeader'` — all 7 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ction (#1926) ## Summary Adds the authoring experience for dashboard sections (create, rename, delete, manage tiles) and introduces a polymorphic `DashboardContainer` abstraction that future-proofs the schema for tabs and groups. Builds on #1900 (core collapsible sections mechanics). Closes #1897. ### Schema: `DashboardSection` → `DashboardContainer` - Renamed `DashboardSectionSchema` → `DashboardContainerSchema` with a new `type` field (`'section'` for now, extensible to `'group'` / `'tab'` later) - `sectionId` → `containerId` on tiles - `sections` → `containers` on dashboards - Updated across all packages: common-utils types, API Mongoose model, app types, import/export utils ### Authoring UX | Action | How | |---|---| | **Create section** | Dashboard `...` overflow menu → "Add Section" | | **Rename section** | Click the title text directly (Kibana-style inline editing) | | **Delete section** | Hover section header → `...` → Delete Section (tiles become ungrouped, not deleted) | | **Collapse/expand** | Click section header chevron | | **Toggle default state** | Hover header → `...` → Collapse/Expand by Default | | **Add tile to section** | Hover section header → `+` button opens tile editor pre-assigned to that section | | **Move tile to section** | Hover tile → grid icon → pick target section from dropdown | | **Move tile out** | Same dropdown → "(Ungrouped)" |  ### UX polish (informed by best practices research) - **Click-to-rename** — click section title text to edit inline (no menu navigation needed) - **Hover-only controls** — `...` menu and `+` button only appear on section header hover, keeping view mode clean - **"Add Section" demoted** — moved from equal-sized button to dashboard overflow menu (section creation is less frequent than tile creation) - **"Move to Section" reordered** — placed before delete button for discoverability, uses `IconLayoutList` instead of `IconFolders` ### What's NOT in this PR (follow-up work) - **Drag tiles between sections** — needs `react-dnd` custom drag layer; data model already supports it (`containerId` update) - **Reorder sections** — needs sortable list library; data model supports it (array order) - **Tabs / Groups** — new container types; just add to the `type` enum and build UIs ## Test plan - [x] 30 unit tests pass (16 existing schema/grouping + 14 new authoring operations) - [x] All 110 dashboard tests pass unchanged - [x] ESLint clean - [x] No TypeScript errors in changed files - [x] Backward compatible — dashboards without containers render exactly as before 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: peter-leonov-ch <209667683+peter-leonov-ch@users.noreply.github.com>
## Summary - Section expand/collapse is now tracked in URL query params (`collapsed`/`expanded`) instead of persisting to the DB on every chevron click - The DB-stored `collapsed` field on `DashboardContainer` becomes the default fallback — what viewers see when opening a dashboard fresh (no URL state) - Chevron click updates URL state only (per-viewer, shareable via link) - "Collapse by Default" / "Expand by Default" menu action in the section header saves to the DB (via `setDashboard`), setting the default for all viewers - `SectionHeader` now accepts separate `collapsed`/`defaultCollapsed` props and `onToggle`/`onToggleDefaultCollapsed` handlers - Adds 7 unit tests for `SectionHeader` Implements Drew's [review feedback on PR #1926](#1926 (comment)): > IMO, expanding/collapsing should not be persisted to the dashboard UNLESS this option is used. [...] I think it would be nice to persist normal expand collapse states in the URL, and then fallback to the default state (saved in the DB based on this option here) if there is no URL state. ## Demo  Shows: expanded sections → chevron click collapses first section (URL updates to `?collapsed=...`) → menu shows "Collapse by Default" (DB action, separate from view state) ## Test plan - [x] Open a dashboard with sections — collapse/expand via chevron click, verify URL updates (`?collapsed=...` / `?expanded=...`) without saving to DB - [x] Copy the URL with collapse state and open in a new tab — verify sections reflect the URL state - [x] Open the section menu and click "Collapse by Default" — verify this saves to DB (persists after page refresh without URL params) - [x] Verify "Expand by Default" / "Collapse by Default" label reflects the DB default, not current view state - [x] Run `yarn ci:unit --testPathPatterns='SectionHeader'` — all 7 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: peter-leonov-ch <209667683+peter-leonov-ch@users.noreply.github.com>

Summary
Adds the authoring experience for dashboard sections (create, rename, delete, manage tiles) and introduces a polymorphic
DashboardContainerabstraction that future-proofs the schema for tabs and groups.Builds on #1900 (core collapsible sections mechanics). Closes #1897.
Schema:
DashboardSection→DashboardContainerDashboardSectionSchema→DashboardContainerSchemawith a newtypefield ('section'for now, extensible to'group'/'tab'later)sectionId→containerIdon tilessections→containerson dashboardsAuthoring UX
...overflow menu → "Add Section"...→ Delete Section (tiles become ungrouped, not deleted)...→ Collapse/Expand by Default+button opens tile editor pre-assigned to that sectionUX polish (informed by best practices research)
...menu and+button only appear on section header hover, keeping view mode cleanIconLayoutListinstead ofIconFoldersWhat's NOT in this PR (follow-up work)
react-dndcustom drag layer; data model already supports it (containerIdupdate)typeenum and build UIsTest plan
🤖 Generated with Claude Code