Skip to content

feat(app): [YW-273] add-remote-source renderer flow — DataSourceDisplay preview/query UI#148

Merged
youhaowei merged 6 commits into
mainfrom
charixandra/yw-273-add-remote-source-renderer-flow
Jun 17, 2026
Merged

feat(app): [YW-273] add-remote-source renderer flow — DataSourceDisplay preview/query UI#148
youhaowei merged 6 commits into
mainfrom
charixandra/yw-273-add-remote-source-renderer-flow

Conversation

@youhaowei

@youhaowei youhaowei commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Wire the Notion renderer flow: replaces NOTION_ENABLED=false / stub pendingSchema() / pendingQueryResult() with real server calls via useNotionMutations() — the full add-remote-source journey is now live end-to-end
  • Un-gate NOTION_ENABLED=true: the deferred banner is gone from the Notion data-source display path; the server seam (listNotionDatabases + queryNotionDatabase) was already complete and tested on main
  • Preview shows column schema: queryNotionDatabase returns { arrowBuffer, fieldIds, fields } — the renderer builds a column list from fields and shows it in the preview card. Rows stay in the Arrow buffer server-side until a visualization materializes them (by design)
  • Property-selection UI removed: the MultiSelectField + schema useEffect + isFetchingSchema/databaseSchema/selectedPropertyIds/rowLimit state are gone — no separate schema endpoint exists on the server, and the query result already carries the full field list. Property filtering is a follow-on (the server query can accept selectedPropertyIds in a future ticket)

Privacy boundary — VERIFIED

DataSourceDisplay.tsx has:

  • No @wystack/secret-vault import
  • No SecretRef, SecretVault, apiKey, or plaintext credential in scope
  • Notion mutations take only dataSourceId (a public UUID) — the server reads the SecretRef from the DB row, mints the bound resolver, and calls the connector. The renderer only receives { arrowBuffer, fieldIds, fields }.

Test plan

  • bun check — 75/75 tasks clean (verified locally)
  • apps/server vitest — 8 files, 236 tests passing (includes notion-routes.test.ts and vault control-plane tests)
  • packages/connector-notion vitest — 2 files, 81 tests passing (includes capability-attenuation / cross-resolver access test)
  • Full typecheck: no cascade errors (full turbo graph checked)
  • Privacy invariant: grep DataSourceDisplay.tsx for secret\|apiKey\|SecretRef\|SecretVault\|plaintext → no hits (only config.hasApiKey presence check)
  • CI green

🤖 Generated with Claude Code

Greptile Summary

This PR wires up the full Notion renderer flow end-to-end: DataSourceDisplay now calls real server mutations (queryNotionDatabase / listNotionDatabases) via useNotionMutations(), replacing the deferred stub code that was gated behind NOTION_ENABLED=false. The NotionDeferredBanner feature-flag file is deleted and NotionDataSourceView + useNotionSync are extracted to keep the main component within its cognitive-complexity budget.

  • rowCount added to ConnectorQueryResult and threaded through connector-notion, connector-rest, app-artifacts, and the app-data client type so the renderer can display accurate row counts and register DataFrame metadata.
  • Optional limit parameter added to queryNotionDatabase (server + hook) with a defensive guard that rejects limit ≤ 0; three new tests cover the wiring, unbounded fetch, and zero-limit rejection.
  • materializeNotionTable persists the Arrow buffer to IndexedDB and updates the DataTable record (fields + dataFrameId + lastFetchedAt) before showing the success toast, so synced data survives a reload; handleSyncData and handleRefreshDataTable are unified into runNotionQuery eliminating ~30 lines of duplication; stale preview on table switch is suppressed by keying PreviewData on tableId.

Confidence Score: 5/5

Safe to merge — the renderer-to-server Notion query path is complete and correct, all previous review findings are addressed, and the new persistence logic is well-guarded.

The rowCount fix, stale-preview suppression, handler deduplication, and missing column-guard are all resolved from prior rounds. The materializeNotionTable function correctly handles both first-sync and re-sync cases (replace-in-place vs. add). The optional limit parameter has a correct non-positive guard backed by three targeted tests. No off-token colors, no ticket IDs in source, and the privacy invariant (no credentials in renderer scope) is maintained throughout.

No files require special attention.

Important Files Changed

Filename Overview
packages/app/src/components/data-sources/DataSourceDisplay.tsx Large refactor: NotionDataSourceView + useNotionSync extracted, runNotionQuery deduplicates sync/refresh handlers, tableId-keyed preview suppresses stale data on table switch, materializeNotionTable handles IndexedDB persistence. All previous review issues resolved.
apps/server/src/functions/app-artifacts.ts Adds optional limit param to queryNotionDatabase with correct 0/negative guard; adds rowCount to NotionQueryResult and passes it through from connector result.
apps/server/src/functions/notion-routes.test.ts Three new tests cover limit forwarding, unbounded fetch, and limit:0 rejection. queryCalls spy correctly validates pagination options passed to connector.query.
packages/app-data/src/notion.ts Adds optional limit to queryDatabase signature; uses loose() wrapper to strip undefined before mutateAsync. rowCount added to NotionQueryResult interface.
packages/engine/src/connector/types.ts Adds rowCount to ConnectorQueryResult interface with clear doc comment. Breaking change propagated to both connector-notion and connector-rest.
packages/connector-notion/src/connector.ts Forwards conversionResult.rowCount in the serializable query return — minimal one-line addition completing the ConnectorQueryResult interface change.
packages/connector-rest/src/connector.ts Adds rowCount: limitedRows.length to the rest connector's query result, completing interface compliance.
packages/app/src/components/data-sources/NotionDeferredBanner.tsx Deleted — feature-flag file removed now that the Notion integration is fully live.
packages/app/src/components/data-sources/DataSourceControls.tsx Removes NOTION_ENABLED / NotionDeferredBanner imports and isNotionDeferred guards; Notion API key + tables sections now always render for notion-type sources.
packages/connector-notion/src/connector.test.ts Adds assertion that rowCount is a number in the serializable query result, keeping the capability-attenuation test up to date with the interface change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as NotionDataSourceView
    participant Hook as useNotionSync
    participant AppData as useNotionMutations
    participant Server as queryNotionDatabase (server)
    participant Connector as NotionConnector
    participant IDB as IndexedDB (DataFrame)

    UI->>Hook: handleSyncData()
    Hook->>Hook: runNotionQuery()
    Hook->>AppData: queryDatabase(dataSourceId, databaseId, tableId)
    AppData->>Server: "mutateAsync({ dataSourceId, databaseId, tableId })"
    Server->>Connector: connector.query(databaseId, tableId, pagination?)
    Connector-->>Server: "{ arrowBuffer, fieldIds, fields, rowCount }"
    Server-->>AppData: NotionQueryResult
    AppData-->>Hook: result
    Hook->>Hook: filter fields (strip _-prefixed)
    Hook->>IDB: materializeNotionTable(table, result, name)
    IDB->>IDB: DataFrame.create(bytes, fieldIds)
    IDB->>IDB: replaceDataFrame OR addDataFrameEntry
    IDB->>IDB: updateDataTable(fields, dataFrameId, lastFetchedAt)
    IDB-->>Hook: "{ dataFrameId, rowCount, columnCount }"
    Hook->>Hook: "setNotionPreviewData({ tableId, columns, rowCount })"
    Hook-->>UI: "isRefreshing=false, notionPreviewData updated"
    UI->>UI: "currentPreviewData = previewData if tableId matches"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as NotionDataSourceView
    participant Hook as useNotionSync
    participant AppData as useNotionMutations
    participant Server as queryNotionDatabase (server)
    participant Connector as NotionConnector
    participant IDB as IndexedDB (DataFrame)

    UI->>Hook: handleSyncData()
    Hook->>Hook: runNotionQuery()
    Hook->>AppData: queryDatabase(dataSourceId, databaseId, tableId)
    AppData->>Server: "mutateAsync({ dataSourceId, databaseId, tableId })"
    Server->>Connector: connector.query(databaseId, tableId, pagination?)
    Connector-->>Server: "{ arrowBuffer, fieldIds, fields, rowCount }"
    Server-->>AppData: NotionQueryResult
    AppData-->>Hook: result
    Hook->>Hook: filter fields (strip _-prefixed)
    Hook->>IDB: materializeNotionTable(table, result, name)
    IDB->>IDB: DataFrame.create(bytes, fieldIds)
    IDB->>IDB: replaceDataFrame OR addDataFrameEntry
    IDB->>IDB: updateDataTable(fields, dataFrameId, lastFetchedAt)
    IDB-->>Hook: "{ dataFrameId, rowCount, columnCount }"
    Hook->>Hook: "setNotionPreviewData({ tableId, columns, rowCount })"
    Hook-->>UI: "isRefreshing=false, notionPreviewData updated"
    UI->>UI: "currentPreviewData = previewData if tableId matches"
Loading

Comments Outside Diff (2)

  1. packages/app/src/components/data-sources/DataSourceDisplay.tsx, line 352-447 (link)

    P2 handleSyncData and handleRefreshDataTable are near-identical

    Both handlers call notionMutations.queryDatabase with the same arguments, apply the same fields filter/map, write the same notionPreviewData shape, and call tableMutations.refresh. The only difference is the success toast message. Extracting the shared logic into a private runNotionQuery helper would eliminate ~30 lines of duplication and make future changes (e.g. surfacing row count when the API returns it) a one-line edit.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/app/src/components/data-sources/DataSourceDisplay.tsx
    Line: 352-447
    
    Comment:
    **`handleSyncData` and `handleRefreshDataTable` are near-identical**
    
    Both handlers call `notionMutations.queryDatabase` with the same arguments, apply the same `fields` filter/map, write the same `notionPreviewData` shape, and call `tableMutations.refresh`. The only difference is the success toast message. Extracting the shared logic into a private `runNotionQuery` helper would eliminate ~30 lines of duplication and make future changes (e.g. surfacing row count when the API returns it) a one-line edit.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code Fix in Codex Fix in Cursor

  2. packages/app/src/components/data-sources/DataSourceDisplay.tsx, line 444-461 (link)

    P1 handleRefreshDataTable is missing the !columns.length guard that handleSyncData has (line 395). If a refresh returns a Notion database where every field name begins with _ (e.g. after a schema change), columns will be empty, setNotionPreviewData will overwrite the previously valid preview with { rows: [], columns: [] }, tableMutations.refresh will update the timestamp, and the user sees "Data refreshed successfully" — but the preview card goes blank with "0 columns". handleSyncData avoids this by bailing out early and leaving the existing preview intact.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/app/src/components/data-sources/DataSourceDisplay.tsx
    Line: 444-461
    
    Comment:
    `handleRefreshDataTable` is missing the `!columns.length` guard that `handleSyncData` has (line 395). If a refresh returns a Notion database where every field name begins with `_` (e.g. after a schema change), `columns` will be empty, `setNotionPreviewData` will overwrite the previously valid preview with `{ rows: [], columns: [] }`, `tableMutations.refresh` will update the timestamp, and the user sees "Data refreshed successfully" — but the preview card goes blank with "0 columns". `handleSyncData` avoids this by bailing out early and leaving the existing preview intact.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex Fix in Cursor

Reviews (5): Last reviewed commit: "fix(connector-rest): return rowCount fro..." | Re-trigger Greptile

@linear-code

linear-code Bot commented Jun 17, 2026

Copy link
Copy Markdown

YW-273

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@youhaowei, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 33 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e5a9cd7-4cdd-4f46-9255-e3d6650b38a9

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea9a35 and 8f60958.

📒 Files selected for processing (11)
  • apps/server/src/functions/app-artifacts.ts
  • apps/server/src/functions/notion-routes.test.ts
  • packages/app-data/src/notion.ts
  • packages/app/src/components/data-sources/DataSourceControls.tsx
  • packages/app/src/components/data-sources/DataSourceDisplay.tsx
  • packages/app/src/components/data-sources/NotionDeferredBanner.tsx
  • packages/app/src/components/providers/ConnectorSetup.tsx
  • packages/connector-notion/src/connector.test.ts
  • packages/connector-notion/src/connector.ts
  • packages/connector-rest/src/connector.ts
  • packages/engine/src/connector/types.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx
Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 756cd4b54c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx Outdated
Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx Outdated
Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx
Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx Outdated
Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx
Comment thread packages/app/src/components/data-sources/DataSourceDisplay.tsx Outdated
@youhaowei

Copy link
Copy Markdown
Owner Author

Resolved all open review threads in commit 3317577 (head 3317577):

  • P1 — persist frame before refresh: new materializeNotionTable() decodes the server's base64 Arrow buffer → DataFrame.create (IndexedDB) → registers/replaces the DataFrame entry → links it to the DataTable, BEFORE reporting success. The old timestamp-only refresh() is gone.
  • P1 — save fields on the data table: updateDataTable({ fields, dataFrameId, lastFetchedAt }) now writes the real schema. After add/sync the table has fields + a durable frame.
  • P2 — field count as row count / rowCount: server result now carries a real rowCount (from the Arrow conversion); the preview uses it. fieldIds.length is only used for columnCount.
  • P2 — limit the query: queryNotionDatabase gains an optional limit arg, clamped to positive integers server-side (so limit:0 can't degrade to an unbounded scan). Sync materializes the full database by design (the persisted table must be complete); the limit primitive stays for a future preview-only fetch.
  • P2 — clear preview on table change: preview is keyed by tableId; currentPreviewData is null when it doesn't match the selection.
  • Dead flag/import cleanup: NOTION_ENABLED was always true → removed the flag, the NotionDeferredBanner component, and the stale ConnectorSetup comment.

Verification: an added Notion source now survives a reload with data AND schema intact (frame in IndexedDB + tracked entry, fields + dataFrameId on the table). Local gate: turbo check 75/75. Added route tests for limit forwarding, unbounded default, and the limit:0 clamp.

youhaowei and others added 6 commits June 17, 2026 08:45
…w/query UI

Wire the Notion data-plane in DataSourceDisplay: replace the NOTION_ENABLED=false
gate and pendingSchema/pendingQueryResult stubs with real server calls via
useNotionMutations() from @dashframe/core. The sync path calls
queryNotionDatabase server-side (credential resolves via the bound resolver,
never enters the renderer) and returns { arrowBuffer, fieldIds, fields } — a
serializable result the UI uses to show a column-schema preview. No credential
or resolver is in renderer scope (verified by type: no secret-vault import,
no apiKey, no ref in DataSourceDisplay.tsx).

Remove property-selection/row-limit UI (MultiSelectField, InputField, schema
useEffect, isFetchingSchema/databaseSchema/selectedPropertyIds/rowLimit state)
and the deferred stubs that threw at runtime. The sync button now calls
queryDatabase directly; preview shows column definitions (rows stay server-side
in the Arrow buffer until a visualization materializes them).

Un-gate NOTION_ENABLED=true: the deferred banner is removed from the Notion
data-source display path now that the server seam is complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The serializable query result (arrowBuffer + fieldIds + fields) carries no
materialized row count — rows stay in the Arrow buffer server-side until a
visualization materializes them. The previous code set rowCount to
fieldIds.length (a column count), producing misleading "12 rows" labels.

Fix:
- PreviewData.rowCount is now optional (undefined = column-schema-only view)
- getPreviewDescription shows "N columns" when rowCount is undefined
- getTableStatsDescription shows "N columns" (no rows × prefix) when
  rowCount is undefined; "No data synced yet" only when columnCount is absent
- Both sync/refresh handlers omit rowCount rather than setting it to the
  field count

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
notionPreviewData was shared across all selected tables — switching from
Table A (synced) to Table B would still show Table A's column schema.

Fix: key PreviewData with tableId and derive currentPreviewData by comparing
notionPreviewData.tableId to selectedDataTable.id; returns null when they
differ. The preview card, header stats, and collapse toggle all use
currentPreviewData so the stale view is suppressed immediately on table
switch rather than waiting for the next sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ionDataSourceView + useNotionSync hook

sonarjs/cognitive-complexity was 16 (limit 15) on DataSourceDisplay after
adding the Notion preview flow. Fix: move Notion-specific state, query logic
and JSX into NotionDataSourceView + useNotionSync so each function stays
within budget. Also extract formatRelativeTime to module scope (was a
useCallback inside the component). DataSourceDisplay becomes a thin
dispatcher (local / notion / unsupported), complexity drops to ~6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…; drop dead flag

Closes the two P1 review findings that left an added Notion source broken on
reload:

- Persist the frame BEFORE reporting success. New materializeNotionTable()
  decodes the server's base64 Arrow buffer, persists it to IndexedDB via
  DataFrame.create, registers (or replaces) the DataFrame entry, and links it
  to the DataTable. Mirrors the local-CSV ingest path. Replaces the old
  refresh() call that only stamped lastFetchedAt and never persisted the frame.
- Save returned fields onto the DataTable via updateDataTable({ fields,
  dataFrameId, lastFetchedAt }), so the table has a real schema after add.

Both now survive a reload with data and schema intact.

Supporting changes:
- ConnectorQueryResult / server + app-data NotionQueryResult carry rowCount
  (single ConnectorQueryResult implementor: NotionConnector — updated).
- queryNotionDatabase gains an optional limit arg, forwarded as pagination.
  Server clamps to positive integers — limit:0 must not become an unbounded
  scan via the connector's `0 || Infinity` page loop.
- Sync materializes the FULL database (no row cap) so large sources persist
  intact; the limit primitive stays for a future preview-only fetch.

Cleanup (no-backward-compat): NOTION_ENABLED is now always true and its banner
branch is dead in every caller — removed the flag, the NotionDeferredBanner
component, and the stale ConnectorSetup comment.

Local gate: turbo check 75/75. Added route tests for limit forwarding,
unbounded default, and the limit:0 clamp; connector test asserts rowCount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eryResult contract

ConnectorQueryResult now requires rowCount (added for the Notion frame-persist
flow). connector-rest is the second implementor of query() — return
limitedRows.length so the shared contract holds. Surfaced by CI after rebasing
onto main, which introduced connector-rest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@youhaowei youhaowei force-pushed the charixandra/yw-273-add-remote-source-renderer-flow branch from 3317577 to 8f60958 Compare June 17, 2026 15:47
@youhaowei youhaowei merged commit d171b92 into main Jun 17, 2026
6 checks passed
@youhaowei youhaowei deleted the charixandra/yw-273-add-remote-source-renderer-flow branch June 17, 2026 19:47
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.

1 participant