feat(canvas): render dashboard/web-analytics on the React tier, Quill-styled#2809
feat(canvas): render dashboard/web-analytics on the React tier, Quill-styled#2809adamleithp wants to merge 7 commits into
Conversation
…-styled Make the HTML/React canvas tier the home for PostHog data dashboards. - Point dashboard + web-analytics templates at the freeform (React-in-iframe) renderer via REACT_TIER_TEMPLATE_IDS; add freeformSystemPromptFor() so they get opinionated React prompts while plain freeform stays a generic sandbox. Legacy json-render canvases are untouched (kind set at create, no migration). - Style the sandbox: load Quill's compiled CSS + tokens into the iframe, add the Tailwind Play CDN with a token-mapped config, and disable Preflight (its unlayered form reset was overriding Quill's @layer component styles). Add lucide-react to the import whitelist. - Prompt rules (data templates only): Quill for everything, default outline buttons, Base UI conventions, never restyle Quill components, in-canvas Quill DateTimePicker, half-open toDateTime() windows, SkeletonText loading states. - Context tab: Chat | Context Quill tabs with a CodeMirror markdown editor; context persists per-canvas/per-version and is prepended to every agent turn; editing creates a version. - Refresh button reloads the canvas iframe (re-runs ph.query) and drops the polling/settings dropdown; hide the now-inert toolbar date picker for freeform. - Freeze the canvas + suppress the runtime-error banner mid-turn, and add the swooping .quill-section-loading bar so a turn shows progress without flicker. - Bump @posthog/quill + quill-charts to 0.3.0-beta.18. - Recover from reaped freeform agent sessions by recreating + retrying once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
React Doctor found 5 issues in 4 files · 1 error & 4 warnings. Errors
4 warnings
Reviewed by React Doctor for commit |
|
| import { create } from "zustand"; | ||
|
|
||
| // View-state bridge between the toolbar Refresh button and a freeform canvas: | ||
| // the button and the iframe live in separate subtrees, connected only by the | ||
| // canvas thread id. Bumping a thread's nonce reloads its sandbox iframe, which | ||
| // re-mounts the React app and re-runs its `ph.query` calls (fresh data). | ||
| interface CanvasRefreshStore { | ||
| nonces: Record<string, number>; | ||
| bump: (threadId: string) => void; | ||
| } | ||
|
|
||
| export const useCanvasRefreshStore = create<CanvasRefreshStore>()((set) => ({ | ||
| nonces: {}, | ||
| bump: (threadId) => | ||
| set((s) => ({ | ||
| nonces: { ...s.nonces, [threadId]: (s.nonces[threadId] ?? 0) + 1 }, | ||
| })), | ||
| })); | ||
|
|
||
| export function useCanvasRefreshNonce(threadId: string): number { | ||
| return useCanvasRefreshStore((s) => s.nonces[threadId] ?? 0); | ||
| } |
There was a problem hiding this comment.
Nonces accumulate without cleanup
Every bump(threadId) call grows the nonces map by one entry permanently. There is no removal when a dashboard is closed or its thread is reset. For a long-lived session with many canvases opened and refreshed, this is a persistent leak on the global store.
Rule Used: When using global state, consider the trade-offs i... (source)
Learned From
PostHog/posthog#32692
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/stores/canvasRefreshStore.ts
Line: 1-22
Comment:
**Nonces accumulate without cleanup**
Every `bump(threadId)` call grows the `nonces` map by one entry permanently. There is no removal when a dashboard is closed or its thread is reset. For a long-lived session with many canvases opened and refreshed, this is a persistent leak on the global store.
**Rule Used:** When using global state, consider the trade-offs i... ([source](https://app.greptile.com/posthog-org-19734/-/custom-context?memory=619e9241-3e55-409f-ab92-1c8f3d14fffa))
**Learned From**
[PostHog/posthog#32692](https://github.com/PostHog/posthog/pull/32692)
How can I resolve this? If you propose a fix, please make it concise.…ixes
Follows up the React-tier canvas work with the correct data path and fixes
found while testing the live app.
- ph.query now accepts a TYPED query node ({ kind: "TrendsQuery", … }) as well
as an inline HogQL string. Typed nodes run through the same /query/ runner the
PostHog UI uses, so numbers match the product (sessionization, unique users,
breakdowns, math) and the node's dateRange handles the window. HogQL stays as
the escape hatch. Steer the agent (via MCP insights) to prefer typed nodes.
- Fix the result-shape mismatch that rendered every typed-node value as 0:
TrendsQuery returns SERIES OBJECTS, not rows-of-cells. CanvasDataService now
passes typed-node results through untouched and only row-coerces HogQL; the
result contract + prompt document both shapes.
- Date picker: force `compact` on DateTimePicker (its useMediaQuery auto-detect
measures the full-width iframe and picks the wide layout, overflowing the
popover), and complete the iframe Tailwind config with Quill's full token map
(fill-*/chrome/warning/info) + the v4 `not-disabled:` variant so the calendar
(pure-utility, no BEM fallback) renders correctly.
- Add a `canvas-templates` skill documenting the two tiers, the data path, the
result-shape gotcha, and the picker/styling gotchas for the next dev.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # packages/ui/src/features/canvas/components/DashboardRefreshControl.tsx # packages/ui/src/features/canvas/components/WebsiteLayout.tsx # packages/ui/src/features/canvas/freeform/FreeformCanvasView.tsx # packages/ui/src/features/canvas/freeform/FreeformChat.tsx
Turn each channel's "+" new-task button in the sidebar nav into a dropdown with two options. "New task" keeps the existing navigation; "New canvas" reuses the canvas template picker and assigns the canvas to the channel via the existing useCreateAndOpenDashboard plumbing. Extract a controlled NewCanvasDialog (and shared trackAndCreateCanvas) from NewCanvasMenu so the dashboards grid and the sidebar dropdown share one picker. Generated-By: PostHog Code Task-Id: 8c2574d8-f89d-4656-adca-1d5a577e65f6
Move freeform (React) canvas generation off the in-process streaming side-panel and onto a dedicated agent task, mirroring CONTEXT.md: the canvas screen shows a "Generating… View task" state and the agent publishes the result via the new desktop-file-system-canvas-partial-update MCP tool. - Add buildFreeformGenerationPrompt + useGenerateFreeformCanvas (createTask → file into channel → record task in meta.generationTaskId). - Track the generation task in the canvas record's meta; add dashboards.setGenerationTask mutation (meta-merge, no clobber). - Rewire FreeformCanvasView: drop the chat panel, add the generating state + View task link + FreeformGenerateBar composer; poll the record and adopt the published version via store syncFromRecord. Keep undo/redo/revert/autosave. - Remove the streaming path: freeform-gen service/router, FreeformChat, freeformSubscription, freeformStreamParser, and all DI/router wiring. Generated-By: PostHog Code Task-Id: f23b35ef-22d8-4e82-9798-acb9224a783c
Makes the HTML/React canvas tier (the freeform sandboxed iframe) the home for PostHog data dashboards, fully Quill-styled, with the correct data path — and fixes the rough edges found while testing the live app.
Architecture
dashboard+web-analyticstemplates now render React in the sandbox, not json-render. Routed byREACT_TIER_TEMPLATE_IDS;kind:"freeform"is set at create time, so existing json-render canvases are untouched (no migration).freeformSystemPromptFor(templateId)registry — those two templates get opinionated React prompts, while plain freeform/blank stay a generic "anything goes" sandbox.templateIdis threaded generate → store → service.Data path — typed query nodes (the right way)
ph.queryaccepts a typed query node ({ kind: "TrendsQuery", … }) as well as inline HogQL. Typed nodes run through the same/query/runner the PostHog UI uses, so the numbers match the product (sessionization, unique users, breakdowns, math, bounce rate) and the node'sdateRangehandles the window — no hand-written time SQL. HogQL stays as the escape hatch. The agent is steered (via MCP insights) to prefer typed nodes.CanvasDataServicepasses typed-node results through untouched and only row-coerces HogQL. The contract + prompt document both shapes. (Without this every typed-node value rendered0.)ph.run(insightShortId)stays stubbed — it's the view/published tier's model (allowlisted saved insights), to be wired at publish, not in edit.Sandbox styling (Quill renders correctly in the iframe)
<head>; add the Tailwind Play CDN with the full Quill token map (primary/card/muted/fill-*/chrome/warning/info) + the v4not-disabled:variant, and disable Preflight (its unlayered form reset overrode Quill's@layer componentsstyles). This matters most for pure-utility components likeDateTimePicker, which have no BEM fallback.lucide-reactto the import whitelist.Prompt rules (data templates only)
<select>/<button>); defaultButton variant="outline"; Base UI conventions; never restyle Quill components.DateTimePicker, alwayscompact(itsuseMediaQueryauto-detect measures the full-width iframe and picks the wide layout, overflowing the popover). The dead toolbar picker is hidden for freeform.SkeletonTextloading/refresh placeholders per data point.UI features
Tabs(Chat | Context) with a CodeMirror markdown editor (ContextEditor.tsx). Context persists per-canvas and per-version, is prepended to every agent turn, and editing it creates a version.ph.query) and drops the polling/settings gear dropdown..quill-section-loadingbar.Backend
@posthog/quill+@posthog/quill-chartsto0.3.0-beta.18.Session not found.Docs
.claude/skills/canvas-templatesskill: the two tiers, the data path + result-shape gotcha, thecompact-picker and Tailwind/Quill styling gotchas, for the next dev.Testing
@posthog/core(1592) +@posthog/ui(754) tests pass.TrendsQuerynodes withcompareFilter(per the new prompt); Quill date picker renders correctly (compact). Found + fixed the all-zeros result-shape bug and the picker overflow during this pass.compactpicker on a fresh build) is pending a stack restart, since the data-path and prompt changes run in the main process.Known follow-ups
useState).ph.runallowlisting — Phase 2.🤖 Generated with Claude Code