Skip to content

BE-705: Move the entities table view onto the table endpoint - #9095

Open
TimDiekmann wants to merge 19 commits into
mainfrom
t/be-705-entities-page-table-view
Open

BE-705: Move the entities table view onto the table endpoint#9095
TimDiekmann wants to merge 19 commits into
mainfrom
t/be-705-entities-page-table-view

Conversation

@TimDiekmann

@TimDiekmann TimDiekmann commented Jul 24, 2026

Copy link
Copy Markdown
Member

🌟 What is the purpose of this PR?

Moves the entities table view off the subgraph query and onto the dedicated queryEntitiesTable endpoint. The table renders from flat endpoint rows, and its data flow lives in a hook decoupled from the subgraph path — the Grid and Graph views stay on the old query, whose hook goes back to serving only them.

🔗 Related links

🔍 What does this change?

  • useEntitiesTableQuery (new): fetches pages through the endpoint, accumulates rows across pages, deep-merges the closed-type maps and definitions, and processes responses in an effect — Apollo's onCompleted swallows exceptions in production. A requestKey resets the accumulation when the filters change, consumed cursors dedupe Apollo's cache-then-network double emission, and a failed page keeps prior rows on screen with its cursor unconsumed so a retry reprocesses it.
  • Error surfacing: contract breaks reach Sentry and the error UI with their message instead of collapsing into a fixed string. A first page without a summary throws like the other contract checks (previously the filter chips loaded forever), pinned types resolving to no version surface as an error without a dead retry button, and the retry banner distinguishes a failed refresh (stale rows shown) from a failed load-more.
  • Type pills: the table passes its endpoint summary into useAvailableTypes through a discriminated SummarySource, so the pills come from the same transaction as the page instead of a second summarize call.
  • Filter builders: the subgraph builder applies the same kind×operator gates as the endpoint builder, so a pill can no longer read as active while the table drops its filter — pinned by a builder-agreement test.
  • Bulk actions: archive/un-archive works on rows via a structural ArchivableEntity slice, so table rows qualify without carrying full entity metadata. One-sided links (an endpoint hidden by permissions) render their visible side.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • The Grid and Graph views still run the subgraph query, so the old data hook and the subgraph filter builder stay alive until they move too.
  • The accumulation fold inside useEntitiesTableQuery is not unit-tested yet — extracting it into a pure function with vitest coverage is a planned follow-up.

🐾 Next steps

  • Move the Grid and Graph views onto dedicated queries, deleting the subgraph path and the type-universe gating
  • Extract the page-accumulation fold from useEntitiesTableQuery and unit-test it

🛡 What tests cover this?

  • build-property-filter-clause.test.ts: value coercion, incomplete and kind-mismatched filters staying inert, and the endpoint/subgraph builder agreement on null-ness
  • The endpoint behavior itself is covered by the integration tests in the base PR

❓ How to test this?

  1. Checkout the branch, run yarn dev, open /entities
  2. Page through a table (scroll to load more), toggle type pills, web filters, archived, and property filters — the counts and rows should stay consistent
  3. Sort by different columns and page again — continuation pages keep the sort
  4. Pin a type via an entity-type page and confirm the table scopes to it

📹 Demo

(screenshots to follow — UI is visually unchanged, the data path is new)

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Jul 30, 2026 3:47pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 30, 2026 3:47pm
petrinaut Skipped Skipped Jul 30, 2026 3:47pm

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the full diff against the actual base branch t/be-705-graph-entities-table-endpoint (this PR is stacked on #9094; endpoint behavior reviewed there, this review covers the frontend migration), with a focus on correctness and performance per BE-705. Items under Important are the ones I'd treat as blocking; Suggestions are non-blocking.

Important

1. Bulk archive/un-archive never refreshes the table once the user has paginated

handleBulkActionCompleted calls tableQuery.refetch() (apps/hash-frontend/src/pages/shared/entities-visualizer.tsx:691), which is Apollo's refetch of the current variables. After the user loads a second page, tableCursor is set, and three independent mechanisms each prevent the refresh from doing anything:

  1. Client dedup discards the response. The refetched page's cursor token is already in consumedCursors, so the processing effect hits if (continues && current.consumedCursors.has(pageCursor)) { return current; } (apps/hash-frontend/src/pages/shared/entities-visualizer/use-entities-table-query.tsx:296) and drops the fresh data entirely. The guard exists to dedupe Apollo's cache-then-network double emission, but it can't distinguish that from a deliberate refresh.
  2. The server snapshot is pinned anyway. The cursor token seals transaction_time/decision_time (libs/@local/graph/postgres-store/src/store/postgres/knowledge/entity/table.rs:352-374), so even without the dedup the refetched page would reflect the pre-mutation snapshot and could not show the archive.
  3. Earlier pages and the summary are never refetched. Only the last page's variables are re-run, and includeSummary: !cursor is false for a cursored request (use-entities-table-query.tsx:232), so totalResultCount also stays stale.

Net effect: archive rows on any table with more than one loaded page and nothing on screen changes — no error, no update — until the user changes a filter/sort/view or reloads. The mutation succeeds server-side, so this reads as a silent failure. The single-page case works only because pageCursor === null makes continues false, which will hide this in casual testing.

The hook's public refetch needs "restart the accumulated sequence" semantics — reset the cursor, clear the accumulation, refetch cursor-less first-page variables — rather than "re-run the last page's variables". Note the in-place tableQuery.refetch() calls at entities-visualizer.tsx:806 and entities-visualizer.tsx:865 have the same problem once paginated.

2. Failed first table page shows a "Try again" button that retries the wrong (skipped) query

On the default /entities table view (unpinned, no selection — the most common page), a failed first queryEntitiesTable page produces a permanently dead retry button:

  • tableQuery.error is mirrored into tableSummaryError (entities-visualizer.tsx:361-363), which flows into the external-mode SummarySource.
  • In useAvailableTypes, external mode reports typeUniverseError = summarySource.error since no summary ever arrived (apps/hash-frontend/src/pages/shared/entities-visualizer/shared/use-available-types.ts:258-259), making typeUniverseBlocksResults true (entities-visualizer.tsx:701-704).
  • The retry onClick branches on typeUniverseBlocksResults before usesTableEndpoint (entities-visualizer.tsx:803-804), so it calls refetchTypeUniverse — which in external mode is still the raw Apollo refetch of the summarizeEntities query (use-available-types.ts:291). That query is skipped (skip: !shouldFetchAvailableTypes, use-available-types.ts:129) and its data is ignored entirely by the external branches; Apollo's refetch punches through skip (the repo documents this and guards it with guardedRefetch in use-entities-visualizer-data.tsx:176-185 — a guard useAvailableTypes lacks).

So clicking "Try again" fires a wasted summarizeEntities request, tableQuery is never refetched, its error never clears, and the error screen persists no matter how many clicks — recreating exactly the dead-retry state the PR description says it set out to eliminate. Either check usesTableEndpoint before the type-universe branch in the onClick, or have useAvailableTypes return a no-op/caller-supplied refetch in external mode so the invalid wiring is unrepresentable.

3. Selecting a type pill drops the noisy-system-type exclusion, inflating the pills and changing row results

When selectedTypeIds is non-null the request sends types: { type: "include", entityTypeIds } and noisySystemBaseUrls is silently dropped — it is only sent in the exclude arm (use-entities-table-query.tsx:207-212). On the endpoint, base-URL exclusions live in the scope filter only for the Exclude variant, and the summary's type maps are computed over the scope before the type filter (table.rs:854-876, scope_summary). Two user-visible consequences:

  • Pills flip on selection. The moment any pill is selected, the first page re-issues (cursor resets on filter change) and its summary — now the pill source via SummarySource — spans the web/archived scope with no noisy exclusion, so User/Machine/Organization/notification/page types (the exact types FE-1238 removed) appear in the pill list and counts, then vanish when the selection is cleared.
  • Rows diverge from the other views. buildEntitiesFilter applies ignoreNoisySystemTypesFilter whenever the type is not pinned, including with a selection active (shared/build-filter.ts:180-182 — the doc comment there explains the multi-type-entity case), so a multi-type entity carrying a selected type plus a noisy type was excluded before but appears now, and the Grid/Graph views (still on the subgraph builder) disagree with the Table view under the same filter state.

The endpoint request should carry the noisy base-URL exclusions alongside a selection (the Include variant would need to accept exclusions, or the client keeps them in scope), or at minimum the summary fed to the pills should be filtered client-side.

4. Every appended page regenerates table data for ALL accumulated rows (quadratic pagination cost)

The accumulation effect builds rows = [...current.rows, ...response.rows] and then calls generateTableDataFromEndpointRows over the entire accumulation, not just the new page (use-entities-table-query.tsx:313-337), synchronously inside the setAccumulated updater. Per row the generator does real work — getClosedMultiEntityTypeFromMap, a loop over every property in the closed type schema, allOf ancestor walks, two format() date calls, generateEntityLabel fallback — plus re-running the getTextWidth canvas measurement for every property column each fold. With limit: 500, page k reprocesses k×500 rows: at 20 pages each further load-more stalls the main thread re-deriving 10k+ already-rendered rows whose output cannot change. The deleted use-entities-table-data.tsx was deliberately incremental (generate for the new page only; append rows, union columns/sets).

Since the PR already plans to extract the fold for unit testing, extracting it as an incremental merge fixes both at once. The only genuinely whole-set outputs — the shared-type-title intersection and the all-rows-miss-source/target column hiding — can be carried as running aggregates/counters as the old code did.

5. A cursor surviving a pinned-type prop change builds a summary-less accumulation with no error — filter chips spin forever

resetCursors is wired to every in-component mutation (filters, sort, view, conversions — entities-visualizer.tsx:218-259) but nothing clears tableCursor when the entityTypeId/entityTypeBaseUrl props change, and EntitiesVisualizer is rendered without a key at both call sites, so Next.js query-param-only navigation (e.g. /entities?entityTypeIdOrBaseUrl=X/entities, or between two type pages' entities tabs) keeps the component — and the stale cursor — mounted. The hook then sends the foreign cursor with includeSummary: !cursor = false. In the processing effect, continues is false (new requestKey) but pageCursor !== null, so the missing-summary guard if (pageCursor === null && !response.summary) (use-entities-table-query.tsx:307) is bypassed and a fresh accumulation is stored with summary: null (use-entities-table-query.tsx:338) — plus the rows are the foreign cursor's continuation page, silently skipping the first ~500 rows.

No error is raised: totalResultCount stays null and in external mode useAvailableTypes reports loading forever (summarySource.summary === null && !summarySource.error, use-available-types.ts:284) — exactly the endless-loading state the summary guard was added to prevent, since every follow-up request also carries a cursor. The hook should refuse (or clear) a cursor that wasn't issued for the current requestKey — e.g. only include cursor in the variables when it was produced by an accumulation with a matching key.

Suggestions

  • hadCachedContent is computed but never consumed. It runs a synchronous apolloClient.readQuery of the full 500-row response on every variables change (use-entities-table-query.tsx:368-376), is declared on the result type (use-entities-table-query.tsx:54) and churns the returned memo's identity, yet entities-visualizer.tsx never reads tableQuery.hadCachedContent and no other consumer exists. It appears copied from the subgraph hook's shape. Since this PR defines the new hook's contract from scratch, dropping the field and the cache read is free.
  • cache-and-network guarantees a wasted 500-row server query for any replayed continuation page. On a cache hit for a cursor-bearing page, the cache pass appends the page and marks the cursor consumed; the network pass then arrives and is unconditionally discarded by the guard at use-entities-table-query.tsx:296 (the file's own doc comment on consumedCursors anticipates this double emission). Since a continuation page is pinned to the first page's snapshot by design, freshness adds nothing — use cache-first for cursor-bearing pages, or process the re-emission by replacing the page in place.
  • Row data is retained in multiple copies with no eviction. AccumulatedPages.rows (use-entities-table-query.tsx:76) keeps every raw page solely so the full-table regeneration can re-run (the incremental fix in Important #4 removes this need), tableData.rows holds derived wrappers, and each cursor page's complete scalar response is cached under distinct request variables in Apollo with no keyArgs/eviction policy — so per-cursor cache entries (including abandoned filter sequences') grow unboundedly for the session. A type policy that avoids caching cursor pages, or evicts consumed ones, would cap this.
  • The processing effect is keyed on hideColumns array identity (use-entities-table-query.tsx:366): a caller passing a literal array would loop regeneration on every render for first pages (the PR knows — entities.page.tsx adds a useMemo with a comment saying exactly that), while on continuation pages a changed hideColumns silently doesn't apply due to the consumed-cursor guard. Column hiding is cheap post-processing over columns (generate-table-data-from-endpoint-rows.ts:132-151); deriving visible columns in a separate memo outside the accumulation removes both the identity hazard and the asymmetry.
  • A failed same-variables refresh is mislabelled as a failed load-more. dataIsStale is only true when accumulated.requestKey !== requestKey (use-entities-table-query.tsx:405), so when the bulk-action tableQuery.refetch() rejects, the banner at entities-visualizer.tsx:856 says "Something went wrong loading more entities" even though the on-screen rows are stale pre-mutation results — the "showing the previous results" wording is exactly what applies but is unreachable here. An in-flight-refresh flag would let the banner label it correctly.
  • hasPinnedTypes + resolvedPinnedEntityTypeIds is a two-field invariant the type system doesn't enforce (use-entities-table-query.tsx:143): the request builder uses resolvedPinnedEntityTypeIds ?? ... without consulting hasPinnedTypes, so false + non-null array silently pins, while true + empty array flips unresolvablePins. A discriminated parameter (e.g. pinnedTypes: null | { resolvedIds: VersionedUrl[] | null }, mirroring the SummarySource pattern this PR introduces) makes the invalid states unrepresentable. No live bug — the sole caller maintains the invariant today.
  • The builder-agreement test hand-picks 9 cases and leaves most of the closed operator space unpinned (build-property-filter-clause.test.ts:86). Its own comment states the invariant absolutely ("the two must never disagree on null-ness" — isPropertyFilterActive answers for both paths via one builder), yet notEquals, greaterThanOrEqual, lessThan, lessThanOrEqual, isEmpty, isTrue, isFalse, the entire boolean kind, and the whitespace-only number branch (build-property-filter-clause.ts:28) are never exercised. Because the gates are duplicated per-operator in both builders, a divergence in any untested branch passes this suite while producing exactly the failure the test exists to prevent. Both unions are small and closed — enumerating operators × kinds × a fixed value set fully pins the invariant at negligible cost.
  • The rewritten row-to-table transform has zero tests (generate-table-data-from-endpoint-rows.ts:190), despite being a pure function (bar a one-line getTextWidth mock) and despite this same PR adding a vitest suite for a sibling pure helper. None of the PR-new behavior is exercised: the endpointRow.label ?? generateEntityLabel(...) fallback, linkEndpointCell falling back to endpoint.entityId for permission-hidden endpoints (the PR body's advertised one-sided-link behavior), the index > 0 shared-title guard, the page-vs-endpoint archived split, the empty-rows edge of source/target column hiding, and the three throw new Error contract checks feeding the new Sentry/error surfacing. A table-driven suite with a few synthetic EntityTableRow fixtures would pin all of these.
  • The already-pure merge helpers need not wait for the deferred fold extraction. The Known Issues section defers unit-testing the accumulation fold, which is reasonable for the React-coupled parts — but mergeClosedMultiEntityTypeMaps (use-entities-table-query.tsx:91) and mergeDefinitions (use-entities-table-query.tsx:114) are pure module-level functions testable with just an export. The recursive merge has real edge behavior worth pinning (source page's schema silently wins on collision at line 102); a regression corrupts the closed-type map for every multi-page table and nothing would catch it.

Strengths

  • The core BE-705 goal lands: the table view truly skips the heavy subgraph path (use-entities-visualizer-data.tsx skips both the summarize and subgraph queries when view === "Table"), the summary is requested only on the first page (includeSummary: !cursor), and the type pills reuse it through the discriminated SummarySource — eliminating a full extra round trip and sourcing pills from the same transaction as the page.
  • The page-accumulation state machine is carefully reasoned for its main hazards: consumed-cursor dedup neutralizes Apollo's cache-then-network double emission (safe precisely because the endpoint cursor pins the snapshot), a stale accumulation never offers its continuation (cursor gated on isCurrent), a failed page leaves its cursor unconsumed so retry genuinely reprocesses it, and the requestKey guard quarantines late responses from previous filter sets (use-entities-table-query.tsx:292-343).
  • Processing responses in an effect instead of Apollo's production-exception-swallowing onCompleted, with explicit contract-break guards (missing definitions/closed types/first-page summary) surfaced to both the error UI and Sentry, closes a real invisible-failure class the old updateTableData path had.
  • The builder-agreement test plus the kind×operator gates retrofitted onto buildPropertyFilterClause fix a genuine pre-existing bug (string parameters sent to greater/less) and pin the invariant that isPropertyFilterActive answers for both query paths.
  • The shared-type-title logic fixes the old code's index-0 self-wipe (the prune loop ran against an empty set on the first entity, so the header was always "Entity"); the index > 0 guard makes the feature work for the first time.
  • One-sided link handling is more correct than before: per-side noSource/noTarget counting replaces the old both-or-nothing linkData branch, and endpoint types are verifiably present in the response's closed-type maps, so linkEndpointCell's lookups are safe.
  • Per-row client work drops meaningfully: rows prefer the server-materialized endpointRow.label, and link endpoint cells come from denormalized endpoint fields instead of subgraph traversal (getEntityRevision/generateLinkEntityLabel removed).
  • ArchivableEntity is a well-judged structural slice — the doc comment explains precisely why it is not a Pick of Entity, and the isType discriminant remains sound for the new union in BulkActionsDropdown.
  • Hot identities are handled deliberately: the closedMultiEntityTypes memo dedupes by sorted type-id key instead of recomputing per row, and resetCursors/conversions/hideColumns are memoized at the call sites to keep the query variables stable.

Generated by Claude Code

@TimDiekmann
TimDiekmann force-pushed the t/be-705-entities-page-table-view branch from 2222708 to 10edad5 Compare July 25, 2026 11:55
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 25, 2026 11:55 Inactive
@TimDiekmann
TimDiekmann force-pushed the t/be-705-entities-page-table-view branch from 10edad5 to 71e0bfa Compare July 25, 2026 12:33
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 25, 2026 12:33 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 25, 2026 12:46 Inactive
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 25, 2026 12:53 Inactive
@github-actions github-actions Bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team labels Jul 25, 2026
@TimDiekmann
TimDiekmann force-pushed the t/be-705-entities-page-table-view branch from 02952e2 to fe51fe8 Compare July 25, 2026 12:54
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 25, 2026 12:54 Inactive
@github-actions github-actions Bot removed area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team labels Jul 25, 2026
@TimDiekmann
TimDiekmann marked this pull request as ready for review July 25, 2026 13:02
@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large orchestration change to the main entities UI (dual data paths, pagination, errors, filter/summary coupling); behavior is heavily documented but the accumulation logic in useEntitiesTableQuery is not yet unit-tested per the PR notes.

Overview
Moves the entities Table view onto the dedicated queryEntitiesTable GraphQL path while Grid and Graph keep queryEntitySubgraph.

useEntitiesTableQuery (new) pages through the endpoint, folds rows into EntitiesTableData, merges type definitions across pages, and resets accumulation when filters change (requestKey). Response handling runs in a useEffect (not Apollo onCompleted) so processing failures surface as error, go to Sentry, and can leave prior rows visible (dataIsStale) with retry/restart. Cursor handling avoids continuing the wrong sequence after filter changes.

EntitiesVisualizer wires Table to the new hook (sort mapping, pagination, counts, conversions) and subgraph data only for non-Table views. The first table page’s type summary is fed into useAvailableTypes via SummarySource to avoid a duplicate summarizeEntities call for filter chips. Bulk actions build lightweight ArchivableEntity objects from selected rows.

Removes client-side subgraph → table assembly (useEntitiesTableData, generateTableDataFromRows). Adds buildEndpointPropertyFilter and tightens subgraph filter builders so “active” property pills match what each API accepts (covered by tests). Small refactors: displaysFilesOnly, stable hideColumns on the entities page, gridHeaderBaseFont moved to grid utils.

Reviewed by Cursor Bugbot for commit 4b0412e. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
@github-actions github-actions Bot removed area/apps > hash-api Affects the HASH API (app) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team area/tests New or updated tests labels Jul 30, 2026
Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 30, 2026 12:45 Inactive
Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The Package (@apps/hash-frontend) red on fba4cef is from the new displays-files-only files:

  • lint:tsc: both displays-files-only.ts:4 and its test import ClosedMultiEntityType from @blockprotocol/graph, but that package has no such export — it lives in @blockprotocol/type-system (where entities-visualizer.tsx imports it from). Folding it into the existing @blockprotocol/type-system type import fixes it.
  • lint:eslint: most likely no-unnecessary-type-assertion on the as BaseUrl[] at displays-files-only.ts:27 and the as BaseUrl at test.ts:51 — the entityTypeBaseUrl values are already typed BaseUrl, so both casts are redundant (the original inline code had no cast there). Inferred from static analysis; the raw eslint output wasn't reachable from here.

Generated by Claude Code

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

e44b01e cleared the tsc errors, but lint:eslint still fails with exactly one error (from the job log this time):

entities-visualizer.tsx  386:11  error  Unnecessary conditional, value is always falsy  @typescript-eslint/no-unnecessary-condition

That's the if (!firstTypeId) guard in the closedMultiEntityTypes memo. The ternary now types rowTypeIdLists as the union VersionedUrl[][] | [VersionedUrl, ...VersionedUrl[]][] (branch 2's entity.metadata.entityTypeIds is a non-empty tuple), and destructuring [firstTypeId, ...rest] from an element of that union loses the | undefined that noUncheckedIndexedAccess would normally add — reproduced with TS 5.9.3. So eslint sees the guard as dead code while tsc is happy.

Verified fix: annotate branch 2's callback so both branches are plain VersionedUrl[][]:

rowTypeIdLists:
  entities?.map((entity): VersionedUrl[] => entity.metadata.entityTypeIds) ?? [],

firstTypeId then includes | undefined, the guard becomes type-necessary, and the tuple construction after the guard still typechecks. (Deleting the guard would also satisfy the rule but drops the runtime protection.)


Generated by Claude Code

@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 30, 2026 14:15 Inactive
Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
@vercel
vercel Bot temporarily deployed to Preview – petrinaut July 30, 2026 15:28 Inactive

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4b0412e. Configure here.

Comment thread apps/hash-frontend/src/pages/shared/entities-visualizer.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants