Skip to content

fix(plugin-dashboard): leave a tile out when the server refuses its data to the viewer - #27065

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-r7ngpd
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-r7ngpd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Refs #26971 (item 1: a viewer who cannot read a tile's data does not see a permission-error tile)

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:仪表盘在非设计模式下,服务器以读权限拒绝(403 / PERMISSION_DENIED)某个数据集磁贴的查询后,该磁贴连同卡片框和标题整个不再渲染,其余磁贴自动重排。
  • 为什么改:HotCRM 销售业绩仪表盘里,无权读取某数据集的用户会看到一块「无权查看」的错误磁贴,应用无法只对该用户隐藏它(Dashboard widgets have no audience gate: a tile whose dataset the viewer cannot read draws a permission error, and no widget key can hide it from that viewer #26971 验收项 1)。
  • 风险与回滚:仅渲染层;403 仍是唯一判据,不做客户端猜测。设计模式与可编辑网格仍显示「无权访问」状态。回滚即还原 DashboardRenderer.tsx 的两处改动。
  • 先看:ui/packages/plugin-dashboard/src/DashboardRenderer.tsx(refusedTiles 状态与 renderWidget 中的 return null)、DatasetWidget.tsx(onForbidden 回调)、新测试文件。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-dashboard/src/__tests__/DashboardRenderer.refusedTile-26971.test.tsx,预期 3 个用例全部通过。
  • 需要你做的:无,席位落地。

What changes

  • DatasetWidget takes an optional onForbidden callback and calls it when its query is refused as a read refusal (classifyLoadError(e) === 'forbidden'). It still draws the objectui#10899 no-access state itself, so any host that keeps the tile on screen is unchanged. DatasetWidget is not exported from the package entry.
  • DashboardRenderer records each refused tile (keyed by the tile key and the dataset it names) and, outside design mode, renders nothing for it: no Card frame, no title. The desktop layout is span-flow (array order is visual order) and the mobile layout is two stacks, so the remaining tiles reflow with no gap.
  • In design mode the tile stays, selectable, with the no-access state. DashboardGridLayout (react-grid-layout, explicit x/y) is not changed and keeps the no-access state; the console's DashboardView renders DashboardRenderer.
  • Changeset: @object-ui/plugin-dashboard patch.

Why

A viewer holding only some permission sets saw one dashboard tile answer "You don't have permission to view the data behind this widget" among tiles that answered, and the app had no key to hide that one tile. Triage routed the card to target 2 (a read-aware default, no new widget key); the seat chose "leave the tile out" because the quiet cell already exists since objectui#10899.

Tests

New __tests__/DashboardRenderer.refusedTile-26971.test.tsx, two table tiles, one whose dataset query rejects with the refusal shape (httpStatus: 403, code: 'PERMISSION_DENIED'):

  1. View mode: the readable tile renders; the refused tile was queried, and afterwards neither its title nor any no-access text is in the document.
  2. CONTROL, design mode: the refused tile stays (dashboard-preview-widget-quota_attainment) and shows the no-access state.
  3. CONTROL: a non-refusal failure (400) keeps the tile, its title and its error text.

DatasetWidget.forbiddenState-10899.test.tsx stays green (it pins the widget-level state, which stays).

Ablation (head 52ed283)

node scripts/ablation-replace.mjs deleted the line that returns null for a refused tile outside design mode (anchor hit once, blob changed). The view-mode case went red ("drops the refused tile, frame and title included, and keeps the readable one"), both controls stayed green (1 failed / 2 passed). The tool restored the file: blob equals HEAD, git diff HEAD empty. The test imports the renderer by relative path, so no dist leg applies.

Acceptance notes

  • Nothing is hidden before the server answers: the tile shows its normal loading skeleton, then leaves. A refused tile is not re-queried until the dashboard remounts, so a permission granted mid-session shows after a reload.
  • If every tile of a dashboard is refused, the viewer sees the header and an empty grid; no message is drawn for that case.
  • Object-bound inline widgets (an object key, no dataset) are not pre-filtered by the viewer's read permission in this PR (the dispatch's optional DashboardView pre-filter was not built).
  • Acceptance item 2 (the spec describes the behaviour) is outside this PR: it needs a sentence in packages/spec/src/ui/dashboard.zod.ts beside requiresObject, which the seat files as a spec-lane card.

Verification

Local gates (head 52ed283, base cb8988e)
  • Closure build pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-dashboard^...' build under the lock: VERDICT command-exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 commands, all exit 0 (including node scripts/check-sdui-manifest.mjs); --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
  • pnpm --filter @object-ui/plugin-dashboard run type-check: exit 0. pnpm --dir ui run type-check:scripts: exit 0. pnpm exec eslint on the three changed source files: exit 0. pnpm check:changeset-fixed: exit 0. pnpm --dir ui run check:new-line-citations: 0 new citations, exit 0.
  • Whole package pnpm --dir ui exec vitest run packages/plugin-dashboard/ under the lock: 190 test files passed, 1858 tests passed, 6 skipped.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (eslint on the changed files; the repo scan is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief above is Chinese by repo rule).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbacRXrqkMrm5LGBGYE


Generated by Claude Code

…ata to the viewer

Outside design mode, a dataset tile whose query the server refuses as a read
refusal is dropped from the dashboard layout, so the viewer sees no
permission-error tile and the other tiles reflow. Design mode keeps the
no-access state so the author can still select the tile.

Claude-Session: https://claude.ai/code/session_01JQwxbacRXrqkMrm5LGBGYE
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head 52ed28395e · 2026-10-28T03:38Z

席位意见:只有服务器真的以 403 拒绝读取时,查看者才看不到那块磁贴,前端不做任何猜测;设计模式和可编辑网格照旧显示无权限状态。spec 说明另立 #27068 交给 spec 车道。

  1. Shape: draft, base main; the body says Refs #26971 (item 1: …), with no closing keyword, as the order requires (Acceptance 2 is split). 4 files, +185/−1. Not governed (check-governed-merges --pr 27065). ≤ 3000 lines.
  2. Premise measured. Seat read at head:
    • DatasetWidget calls the optional onForbidden only in the classifyLoadError(e) === 'forbidden' branch, after setting the [finding] packages/qa/dogfood/tsconfig.json includes src/**/*, a directory the package does not have #10899 state. Any other failure keeps its error.
    • DashboardRenderer keeps a per-render-tree set keyed widgetKey::dataset, and returns null for a refused tile only when !designMode. Frame and title go with it, and the span-flow grid reflows.
    • The callback is stable (useCallback), and the widget's query effect (deps [signature, invalidationNonce]) does not re-run on it.
    • DashboardGridLayout and design mode are untouched, so they keep the tile. Every tile is still queried, and nothing is hidden before the server answers.
  3. Gate check-runs (pinned to head 52ed28395e). In progress at this record; landing waits for every check green. The dev's local reading: 34/34 derived families exit 0, --ran reconciled, 6/6 new tests, package 190/190 files, ablation red then restored.
  4. Accepted deviations. The optional DashboardView mayReadObject pre-filter was not built. The order made it optional, and the HotCRM tile is dataset-bound, so the 403 path covers it.
  5. Out of scope.
  6. Landing clause. Not governed. When every check on the current head is green, the seat lands it (pr_ready + automerge_enable), or asks the maintainer to click if the classifier refuses. On merge, Dashboard widgets have no audience gate: a tile whose dataset the viewer cannot read draws a permission error, and no widget key can hide it from that viewer #26971's item 1 is done and item 2 is carried by spec(ui): the dashboard widget describe does not say that a dataset tile the server refuses as a read refusal is left out for that viewer; requiresObject keeps its install-only meaning #27068, so the seat closes Dashboard widgets have no audience gate: a tile whose dataset the viewer cannot read draws a permission error, and no widget key can hide it from that viewer #26971 as completed, pointing to both.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 08:05
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 08:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 4aeb054 Oct 28, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-r7ngpd branch October 28, 2026 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants