The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
8.2 KiB
Layout and UX
Grid sizing (overview)
Catalog defaultLayout drives default size and resize floors via tileLayouts.ts. Wide tables scroll inside WidgetCardContent, not the dashboard grid.
Tile min/max size (the full guide): § Tile min/max size below — only authoritative section for mins; other docs should link here.
Tile min/max size (grid rows & columns)
Changing minimum tile size is catalog + tileLayouts.ts, not the widget Component. Agents often look in the wrong layer first — use this section.
Mental model
DASHBOARD_WIDGET_CATALOG[].defaultLayout ← you edit this
↓
tileLayouts.ts :: calculateLayouts() ← reads catalog per tile.widget.widget_type
↓
react-grid-layout minW / minH on each item ← enforced while resizing in edit mode
- Grid is 12 columns wide (
w/minWare column counts). - Height is in row units (
h/minH), not columns — users may say “3 rows tall” or “3 grid rows”. - Row height:
BASE_ROW_HEIGHT = 80px infrontend/src/scenes/dashboard/DashboardItems.tsx. Example:minH: 3→ 240 px minimum tile height (plus margins).
Single source of truth
Set mins on the catalog entry only:
// products/dashboards/frontend/widget_types/catalog.ts
defaultLayout: { w: 6, h: 5, minW: 3, minH: 3 },
| Field | Meaning |
|---|---|
w, h |
Default size when adding a widget (dashboardLogic.addWidgetTiles reads these) |
minW, minH |
Smallest size allowed when resizing on the dashboard |
How mins reach the grid
calculateLayouts(tiles)infrontend/src/scenes/dashboard/tileLayouts.tsruns on every dashboard load / tile change.- For widget tiles it calls
getWidgetCatalogLayout(widget_type)→DASHBOARD_WIDGET_CATALOG[key].defaultLayout. getTileMinDimensions()sets RGLminW/minHon each layout item.
Persisted tile JSON (tile.layouts.sm) stores x, y, w, h — not mins. Mins are recomputed from catalog each time, so a catalog minH change applies to existing dashboards without a migration.
Fallbacks (when catalog omits mins)
| Constant | Value | Used when |
|---|---|---|
MIN_WIDGET_TILE_WIDTH_COLS |
3 | Widget tile, catalog has no minW |
MIN_WIDGET_TILE_HEIGHT_ROWS |
4 | Widget tile, catalog has no minH |
MIN_TILE_HEIGHT_ROWS |
2 | Insight tiles |
MIN_TEXT_TILE_HEIGHT_ROWS |
1 | Text tiles |
If a new widget type omits minH, users can only shrink to 4 rows, not 3. Always set explicit minH / minW on the catalog entry.
What does not control dashboard min size
| Location | Why it’s unrelated |
|---|---|
Widget Component / SCSS |
Body layout only; RGL sizes the outer tile |
widget_catalog.py get_default_widget_layouts() |
Backend add helper — returns w/h only, no mins |
Storybook widgetCardStoryFixtures TILE_HEIGHT |
Fixed frame for stories, not RGL |
DashboardWidgetsOverview.stories.tsx heights |
Overview showcase scale, separate from live mins |
Changing MIN_WIDGET_TILE_HEIGHT_ROWS globally |
Affects unknown widget types only; prefer per-type catalog minH |
Change checklist
- Edit
defaultLayout.minH/minWinwidget_types/catalog.tsfor thewidget_type. - Extend
frontend/src/scenes/dashboard/tileLayouts.test.ts— parameterized case withexpectedMinH/expectedMinWfor thatwidget_type(seeerror_tracking_list/session_replay_listexamples). - Run
insightscli test frontend/src/scenes/dashboard/tileLayouts.test.ts. - Manually resize the tile in dashboard edit mode to confirm the floor.
- Optional: add a
MinimumSizestory — frame heightminH * 80px, limit demo data so content fits.
Storybook vs dashboard
- Dashboard: mins from catalog →
tileLayouts.ts→ RGL. - Storybook tile stories: optional fixed frame (
widgetCardStoryFixturesor per-story wrapper). AMinimumSizestory documents the floor; it does not change production behavior.
Add widget modal
- Title: Add widget; description: Bring context from your different Insights products into one dashboard.
AddWidgetModalsupports multi-select — pick one or more catalog variants, then "Add N widgets"dashboardLogic.addWidgetTiles→POST .../widgets/batch/(1–10 tiles)- Backend placement:
widget_layouts.stack_widget_layout_at_bottom— new tiles land at the bottom, anchored to the tallest column so the grid's vertical compaction keeps them there; batch adds stack downward (a horizontal row would drift up on a staircased dashboard). On add,dashboardLogicscrolls#main-contentto the bottom so the new tile is visible. - Placement counts layout-less tiles too:
collect_dashboard_sm_layouts_for_dashboardsynthesizes a bottom placement for every tile withlayouts = {}(e.g. an insight added via the insight API — positioned by the frontend on render, persisted only on a layout save). Without this the backend under-counts the height and drops the widget into a mid-page gap (the "lands in the 2nd row" bug). - REST/MCP add:
dashboard-widgets-batch-add— same batch endpoint; one tile = single-elementwidgets. See mcp.md
NEW badge (Add menu)
Surface the Widget entry as new in the Add menu:
- Populated dashboard:
DashboardHeaderActions—Menuitemtag: 'new' - Empty dashboard:
EmptyDashboardComponent— inline<Tag type="success" size="small">NEW</Tag>on the Widget menu item
Do not add NEW badges on individual catalog variants inside AddWidgetModal.
Config updates
Runtime PATCH flow and save guards: managing-existing-widgets.md § Config update flow. Edit modal field layout: § Edit modal layout there.
Remove and undo
- Remove: no confirm dialog — undo toast (
removeTileSuccessindashboardLogic.tsx) - Copy: "widget removed" + Undo
⋯ menu parity (DashboardWidgetItem)
- View (if
titleHref— title link behavior: composition.md § Header title navigation) - Edit (opens widget settings modal)
- Duplicate
- Show/hide description (toggle visibility; edit text in settings modal)
- Dashboard section: copy/move to another dashboard, remove
- Refresh data — direct click with optional "Last computed" subtitle (
dashboard_tileheader layout; omitted for unknown widget types)
Header layout choice
Prefer headerLayout: 'dashboard_tile' for new widgets — matches insight tile chrome; Refresh data is a direct ⋯ menu item (optional "Last computed" subtitle), same as insight tiles.
See composition.md for header layout details.
Date range in config
Store time period in config.dateRange (insight-shaped { date_from, date_to?, explicitDate? }).
Supported relative date_from values (shortest first): -1h, -3h, -24h, -7d, -14d, -30d, -90d — defined in BE backend/constants.py + Pydantic WidgetDateFrom in widget_specs/common.py. insightscli build:openapi regenerates widget-date-from-options.json; widgetConfigShared.ts re-exports WIDGET_DATE_RANGE_SELECT_OPTIONS (labels) and infers the value type from generated Zod.
Format for display via WidgetCardHeader — reads config.dateRange + catalog headerMeta and formats with dateFilterToText.
Description display
When show_description is enabled, the card header renders markdown under the title (WidgetCardHeader / CardMeta) with max-h-24 overflow-y-auto.