Repository navigation
Fixes #34689: record every Assets tab edit on the asset as its own PATCH - #35026
Conversation
…ags on the glossary term tab
…e and of unordered updates
…olumn propagation
| authorizeBulkAssetsPermission( | ||
| securityContext, request.getAssets(), MetadataOperation.EDIT_TAGS); | ||
|
|
||
| PatchRequester requester = PatchRequester.fromRequest(securityContext, authorizer); |
There was a problem hiding this comment.
Since each asset is now authorized inside the background job, impersonation-based policy conditions stop working on the tag endpoints. isImpersonated() and impersonatedBy(...) read only the ImpersonationContext thread-local (RuleEvaluator.java:614, :634), which is set by JwtFilter on the request thread and isn't copied into the job (RequestLatencyContext.wrapWithContext only carries the latency context).
So a Deny rule like isImpersonated() && matchAnyTag('Sensitivity.Confidential') evaluates to false here, and a bot acting for a user (e.g. an MCP agent) could tag or untag assets that rule is meant to protect. On main the check ran on the request thread, so it applied. Glossary, domain and data product are synchronous, so they're fine.
Setting ImpersonationContext from requester.actor().impersonatedBy() at the start of the job (and clearing it after) should fix it. A test with an impersonated request against such a deny rule would be good to have.
There was a problem hiding this comment.
Confirmed, thanks. The tag tab's job ran without the request's impersonation, so isImpersonated() and impersonatedBy(...) rules were skipped there. d0d9895 sets ImpersonationContext from requester.actor().impersonatedBy() when the job starts and clears it when it ends.
New IT AssetsTabAuthorizationIT.aBotActingForAUser_isRefusedWhatAnImpersonationRuleDenies: a bot impersonating a user adds a tag to two tables, and the user has a deny rule isImpersonated() && matchAnyTag(...) covering one of them. Without the fix the protected table got the tag; with it, only the other table changes.
| .toList(); | ||
| return Response.ok() | ||
| .entity( | ||
| repository.bulkRemoveGlossaryToAssets( |
There was a problem hiding this comment.
A refused removal now fails silently in the UI. Before, a user without permission got a 403 for the whole request and the UI showed an error. Now it's a 200 with failed rows; the add dialog shows those, but the remove handler in AssetsTabs.component.tsx (onAssetRemove) only awaits the call and never looks at the result. So the dialog closes as if it worked, the asset stays, and no error appears. Same for rule failures, and on all four tabs.
Could we handle the BulkOperationResult on remove the same way the add dialog does?
There was a problem hiding this comment.
Agreed, that was a regression. 61e5d64 and c3989f9 make the remove path read the result the way the add dialog does, on all four tabs. For the synchronous tabs it reads the BulkOperationResult. For the tag tab, whose removal runs as a job, it waits up to 2 minutes for the job's outcome on the websocket. When an asset is refused, an error toast shows the reason. If the outcome doesn't arrive in time, or the socket is disconnected, it asks the user to refresh the page to see the result. Tests are in AssetsTabs.utils.test.ts.
| return Response.ok().entity(repository.bulkAddAndValidateGlossaryToAssets(id, request)).build(); | ||
| return Response.ok() | ||
| .entity( | ||
| repository.bulkAddAndValidateGlossaryToAssets( |
There was a problem hiding this comment.
This changes the API response for a user who isn't allowed: before it was a 403 for the whole request, now it's a 200 with status: FAILURE and a failed row (the updated GlossaryTermResourceIT cases assert exactly that). Scripts, SDK users or integrations that only check the HTTP status will now read a refusal as success.
It makes sense with per-asset authorization, but since it's a behaviour change for API clients, could we call it out in the PR description and release notes?
There was a problem hiding this comment.
Agreed. The description now has an "API change" section. It says the tag and glossary term endpoints answer 200, with status failure or partialSuccess and failed rows, where they used to answer 403, so clients must read status. I also added the Backward-Incompatible-Change label so it gets into the 2.1 release notes.
🚦 Removed from the merge queue —
|
…dits-as-patches # Conflicts: # openmetadata-service/src/main/java/org/openmetadata/service/events/ChangeEventHandler.java # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ColumnRepository.java # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java
🚦 Removed from the merge queue —
|
…dits-as-patches # Conflicts: # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java # openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java # openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java # openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java # openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java # openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java # openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java # openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java
Code Review ✅ Approved 2 closed / 2 findings🔴 High risk · Changing tag/glossary bulk endpoints from whole-request 403s to per-asset 200 failures may mislead clients that rely on HTTP status alone. Assets tab tag, glossary term, domain and data product edits now save each asset through the same PATCH path as the asset page, so every edit creates a version, sets "updated by", and emits change events. Resolved that one unresolvable asset no longer fails every asset of its type, and that tag-tab job refusals are now reported instead of going unrecorded after a 120 s wait. No open issues found. ✅ 2 closed✅ Bug: One unresolvable asset fails every asset of its type, including saved ones
✅ Quality: Unrecorded 120 s wait on tag-tab job after which refusals go unreported
VerificationRules No rules evaluated Functional validation Not enabledAuto-approval Not enabledFor more verification details, comment OptionsDisplay: compact → Showing compact verification results. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
| socket?: Socket, | ||
| waitMs = TWO_MINUTE_IN_MILLISECOND | ||
| ) => { | ||
| const finished = new Map<string, BulkAssetsJobOutcome>(); |
There was a problem hiding this comment.
Job outcomes grow without bounds
finished caches every completed job received on the shared channel, including unrelated jobs, without a size limit. It keeps collecting while the removal waits for its response or job outcome. The repository requires every cache to have an explicit size cap, so this must be bounded before merging. Cap the stored entries and discard them when consumed; the timeout does not limit how many outcomes the cache can hold.
Context Used: CLAUDE.md (source)
|
|
…TCH (#35026) * feat(entities): apply a bulk edit as one authorized PATCH per entity * fix(entities): index a patched entity with the fields it inherits * fix(assets): save every Assets tab edit as the asset's own PATCH * fix(search): keep a table's tags and data products off its column entries * docs(decisions): record how bulk edits write, authorize and index * fix(entities): fail only the group that cannot be loaded or recorded * fix(assets): refuse an asset whose labels clash with its glossary's tags on the glossary term tab * refactor(assets): derive the Assets tab status with the shared bulk status rule * test(assets): drop the fail-before-pass gate test that per-asset saves replace * test(assets): compare tab and PATCH labels without their appliedAt * test(assets): keep the search entry checks clear of the response cache and of unordered updates * test(assets): check the column entries of all tables in one search per poll * test(assets): drop the domain move check that waits on main's lossy column propagation * fix(assets): authorize the tag tab job with the request's impersonation * fix(ui): report assets the server refuses to remove from an Assets tab * chore(playwright): auto-refresh impact-map.generated.json * fix(ui): ask to refresh when a tag tab removal's outcome does not arrive * test(glossary): expect a refused bulk asset row instead of a 403 * fix(search): send column entries the glossary terms they inherit from their table --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ts tile padding (open-metadata#35609) * Fix dark-mode status counts, tab counts, table header divider and entity icons - Ingestion/automation status counts use core Badge pills, left-aligned - Active tab counts use an outlined brand Badge instead of a solid fill - Core Table header divider uses border-primary in dark so it stays visible - FeaturedIcon light theme keeps its tinted tile in dark instead of going solid - Avatar gains placeholderIconColor; entity avatars use the brand tint by default * Pin the Connections composer to the canvas edge and record the tinted-icon decision The Connections list footer sat inside the padded PageLayout.Content, so the AI composer stopped 8px above the canvas edge; it now mounts on the page root like Home's footer slot. * Mark Classic mode as deprecated in the mode switchers and the admin setting Classic shows a Deprecated badge with a retirement notice and release-notes link only where a user changes mode (AI-shell switcher, classic profile dropdown) and on the admin Default App Mode setting. * Give Insights tab tiles and DQ pie cards consistent core Card padding Total Data Assets list, Platform Insights KPI cards and the Data Quality pie widgets render their content in Card.Content instead of caller padding (tw:p-3, a gray p-4 box, LESS padding on the DQ dashboard), and the embedded Insights sections drop their surface slab and extra header padding. * Move the status badge color map to IngestionStatusCount.constants.ts * Keep domain and data product avatars solid in both themes Users pick these colors, so design keeps the solid fill until custom colors are reworked. Reverts getEntityAvatarProps and drops the now-unused Avatar.placeholderIconColor; the decision record now covers FeaturedIcon only. * Drop the darker bands from the column detail drawer in dark mode The title block, content wrappers and footer painted @grey-9/@grey-50, which resolve to bg-secondary (darker than the drawer's overlay surface) in dark; they are transparent there now. Key profile metric tiles use an outline instead of the darker fill in dark, and the custom properties list uses bg-surface instead of the page-colour bg-primary. Light mode is unchanged. * Render the contract result pill as a core Badge and fix the Beta tab chip overlap The header Contract Failed / Aborted / Running pill was a secondary Button recolored with ! classes; its 600 text measured 2.7:1 in dark. It is now a shared ContractLatestResultBadge (core Badge palette, 4.7:1 dark / 6.1:1 light) that links to the Contract tab, on both data assets and data products. Core Badge: a linked badge no longer adds tw:text-inherit, which won the tailwind-merge conflict and dropped the badge's own text color. TabsLabel: the Beta chip is no longer absolutely positioned over the next tab. * chore(playwright): auto-refresh impact-map.generated.json * Keep the empty test-status chart inside its card The no-data footer sits in a core Tooltip whose trigger is w-max, so the placeholder SVG resolved its 100% width against max-content (313px) and spilled over the neighbouring cards. The trigger now takes the card width. Also tidies imports left by the contract badge change. * Address review: keep HoverCard open on focus, keep DP contract link on version HoverCard: focus moving from the trigger into the card no longer schedules a close, so a link inside it (the Classic deprecation release-notes link) stays usable from the keyboard. Data product header: the contract result badge keeps the selected version in version history, as the previous handleTabChange navigation did. * fix(ui): converge Archive badge count after an async hard delete (#35446) * fix(ui): converge Archive badge count after an async hard delete A drive file's hard delete answers 202 Accepted and removes the row on a background thread. ContextCenterArchivePage invalidated the archive-count query the moment that request resolved, so the refetch raced the background deletion. When the refetch won, React Query cached the pre-delete total and nothing invalidated the key again, leaving the sidebar badge one too high for the rest of the session. Replace the single fire-and-forget invalidation with reconcileCountAfterAsyncDelete: drop the count optimistically (matching what the page already does to its own list), then refetch until the server agrees or the attempts run out (5 x 1s). Uses refetchQueries rather than invalidateQueries — invalidation only marks an *inactive* query stale without fetching it, so the reconcile would silently no-op whenever the sidebar observing the key is unmounted. On timeout the corrected figure is written back: the last refetch leaves the server's pre-delete total in the cache, and keeping that would restore the staleness this fixes. Deletions still in flight are counted per key so a concurrent delete derives its target from the server's number rather than a cached value a sibling's refetch may have just overwritten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ui): poll the deleted row's absence to converge the Archive badge The reconcile targeted `baseline - started`, a count frozen when the first delete started. Anything else moving the archive count inside the ~5s window threw it off, and the page's own restore handler invalidates that very key: a restore mid-window let the poll see its target early and exit, leaving the badge one too high once the background delete landed — the bug this helper exists to fix. A soft-delete in the window broke it the other way, with `clampedTotal` capping the badge one too low. Use the deleted entity's own absence as the completion signal instead: poll `getContextFileById` until it stops resolving, then refetch once and keep whatever the server reports. No precomputed target, so a concurrent restore or soft-delete cannot skew it, and the baseline/in-flight bookkeeping and the clamp all go away with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ui): probe the archive delete with Include.All and only treat 404 as landed The absence probe called getContextFileById without an include param, but GET /contextCenter/drive/files/{id} defaults to include=non-deleted. Every row on the Archive page is already soft-deleted (the list is loaded with include=deleted), so the probe 404'd before the background hard delete even started. The rejection handler mapped any error to "landed", so the first poll ended the wait: the race this fixes was still there, only pushed back ~1s. Let getContextFileById take an include argument and pass Include.All, so the probe resolves true only once the row is genuinely purged. Narrow the predicate to a 404 as well, leaving a transient error to the next attempt rather than ending the poll early. The helper's unit tests stub hasLanded, so neither gap was visible to them: cover the include wiring in a new assetAPI test and the transient-error case in the helper's own suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * lint fix * fix(ui): mark the archive reconcile float with void Sonar flagged the unawaited reconcileCountAfterAsyncDelete call. The float is deliberate — awaiting it would hold `isDeleting` through the ~5s poll and spin the modal button — so `void` says so explicitly. Nothing escapes: the probe handles both branches and refetchQueries resolves rather than throws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * Fixes #35473: Export and Bulk Edit only the filtered test cases (#35474) * Fixes #26756: Add filter and pagination support to test case export endpoints GET /v1/dataQuality/testCases/name/{name}/export and /exportAsync now accept testCaseStatus, testCaseType, entityFQN, entityLink, tags, tier, owner, limit and offset through one shared @BeanParam, so the two endpoints cannot drift. Matches are resolved through search with the same search_after walk the filtered bundle-suite add uses, so the export selects exactly what the filtered test case list shows (including table-inherited owners); rows are read from the database. Async jobs carry the validated filters as CsvAsyncJobArgs.TestCaseExportArgs, and progress reflects the filtered count. Requests without filters or paging keep the original export path. * Support every test case list filter on the test case export endpoints The export now also accepts q, testPlatforms, dataQualityDimension, serviceName, dataProductFqn and startTimestamp/endTimestamp, so a Bulk Edit opened from a filtered test case list can pass the list's filters through unchanged instead of silently exporting a superset. * Harden filtered test case export coverage and test isolation Validate every CSV row and cover inherited metadata, suite and platform scopes, entity links, status lists, and timestamp validation. Use an export caller per test to avoid concurrent result-retention conflicts, and record the domain-filtered paging contract. * Document test case export filters as query parameters in OpenAPI TestCaseExportQuery's private fields had no getters, so swagger could not see them as properties and documented the @BeanParam as a JSON request body instead of listing the filters as query parameters. * Fixes #35473: Load only the filtered test cases into Bulk Edit Bulk Edit always exported every test case in scope, whatever the list it was opened from was filtered to. The test case list, the table Data Quality tab and the test suite page now carry their active filters into the Bulk Edit URL, and Bulk Edit sends them with the export that loads the grid. The platform list builds its search request and its Bulk Edit filters from the same helper, so the two cannot drift. * Export only the test cases matching the list filters The Export action in the test case manage menu exported everything in scope. It now sends the same list filters as Bulk Edit, so both select the test cases the list shows. * Fix table test case filters after list actions * Fixes open-metadata/openmetadata-collate#7447: forward aria-label on ProgressBarBase; show Deprecated on the Column Bulk Operations page (#35491) * fix(ui): forward aria-label and aria-labelledby on ProgressBarBase ProgressBarBase had a closed props interface and rendered none of the ARIA naming props, so every caller's aria-label was dropped and the progressbar had no accessible name. ProgressBar passes them through too. * feat(ui): support a text badge on sub-nav items SubNavItem gains badgeKey, an i18n key rendered as a text badge in place of the count, so an item can be tagged (e.g. Deprecated). The badge text joins the item's aria-label, which would otherwise hide it. * feat(ui): mark Column Bulk Operations as Deprecated in the classic sidebar Classic sidebar items gain isDeprecated, rendered as a warning badge. Column Bulk Operations switches from the Beta badge to Deprecated. * Revert "feat(ui): mark Column Bulk Operations as Deprecated in the classic sidebar" This reverts commit 3be32e5e66747f6de11bea19ab4d003d95cf7621. * Revert "feat(ui): support a text badge on sub-nav items" This reverts commit 1b1e78bd1fd5937d01a263edb83bf9d44e574756. * feat(ui): show a Deprecated badge on the Column Bulk Operations page header Mark the feature deprecated on the page itself rather than in the navigation: the badge sits next to the title in AI mode and next to the breadcrumb in classic mode. The classic sidebar drops its Beta tag. * fix(ui): align the Deprecated badge with the classic breadcrumb The breadcrumb's bottom margin made the row taller than the text, so the centred badge sat low. Move the margin from the breadcrumb to the row. * feat(ui): explain the Column Bulk Operations deprecation on badge hover Hovering (or keyboard-focusing) the Deprecated badge opens a HoverCard stating the feature is deprecated but remains fully functional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): describe the deprecation to screen readers on the badge The hover card is portalled and absent until opened, so the badge button now points aria-describedby at a visually hidden copy of the message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ui): use the core Tooltip for the Column Bulk Operations deprecation The explanation is non-interactive, so core Tooltip fits better than HoverCard: it wraps the badge in its own focusable trigger and links its content via aria-describedby, removing the raw AriaButton and the visually hidden duplicate description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ui): trim the deprecation Tooltip's class overrides to the minimum Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Fixes #35519: Stabilise flaky glossary Playwright specs (#35520) * Fixes #35519: Stabilise flaky glossary Playwright specs - Related terms: pick the relation type through selectOptionWithRetry and re-pick until the trigger shows it; the bottom-row non-modal Select closed on scroll or took a stale click on the neighbouring option. - addAssetToGlossaryTerm: wait for PUT /assets/add before returning so teardown cannot delete the term mid-add. - Tag label row mapper: tolerate a tag_usage row whose tag/term is gone instead of throwing an NPE that made the asset unreadable and undeletable. - Request tags: open the glossary by FQN so the /glossary auto-select redirect cannot replace the request-tags navigation. - Glossary form tags: centre the trigger before opening so the drawer's late scroll does not close the dropdown. - Glossary import: use saveBulkImport so the toast wait starts before Update. - Bulk edit glossary: clean up on a fresh API context (idle keep-alive socket hang up). * chore(playwright): auto-refresh impact-map.generated.json * Drop positional locators from glossary related-terms helpers and spec Rows are found by the id the add-row click introduces (ids are Date.now()), the relation-type trigger by button[aria-haspopup=listbox], and the row to remove by its trigger's selected value. Prune the now-stale suppressions. * chore(playwright): auto-refresh impact-map.generated.json * Trim review comments to one-line summaries * Lower recorded no-positional-locator suppression count to 552 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(security): bump guava to 33.7.2-jre, hibernate-validator to 8.0.6.Final, java-jwt to 4.6.2 (#35291) * fix(security): manage guava 33.7.2-jre in root pom for all modules #35245 bumped guava only in openmetadata-service; common, spec, sdk and java-client still resolved 33.4.8-jre (dropwizard-jersey) and 33.3.1-android (google-auth-library). CVE-2026-102554. * fix(security): bump hibernate-validator to 8.0.6.Final CVE-2026-93879 (SNYK-JAVA-ORGHIBERNATEVALIDATOR-20877468): EL injection via unescaped message parameters. The fix (HV-2256, 870485e384 'Escape message parameters by default') is contained in 8.0.6.Final; Snyk only lists 9.2.0.Beta1. 8.0.6.Final keeps jakarta.validation-api 3.0.2. * fix(security): bump java-jwt to 4.6.2 CVE-2026-85473 / GHSA-cg7w-5cc2-f2xm (SNYK-JAVA-COMAUTH0-20937619): out-of-range timestamp claims kill token-parsing threads; vulnerable [4.0.0,4.6.2). Bumps the root java-jwt.version and the SDK's jwt.version, and points the integration-tests literal at the shared property. * Fixes open-metadata/openmetadata-collate#4719: let a password reset lift the login lockout (#35523) * Fixes open-metadata/openmetadata-collate#4719: let a password reset lift the login lockout A user who asks an admin for a password reset has usually locked the account with failed logins first, and the reset did not lift the lockout, so the new password was refused until the block expired. Failed logins were counted by email, but the password change and reset paths cleared them by user name and so never cleared anything. The counters also live in each server's memory, so a reset served by one server, or made with the ops CLI, could not reach the counter of the server that answered the next login. Count failed logins against the stored password hash instead. Every login already reads that hash from the database, so a new password set by any path starts with no failures on every server. A correct password clears the failures counted against it, which keeps the MCP login form's reset-on-success working now that failures are no longer keyed by the bare email. The address-only checks that form uses resolve the account first and follow the same rule. The rule is recorded in docs/decisions/2026-10-15-basic-auth-failed-logins-count-against-the-stored-password.md. LoginAttemptCache also wrote failures under the key as given while reading it lowercased; it now lowercases both. The sign-in page now shows the login servlet's error, which it reports as `error` rather than `message`, so a lockout no longer reads as an invalid password. * chore(playwright): auto-refresh impact-map.generated.json * chore(playwright): auto-refresh impact-map.generated.json --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * Fixes #33865: Inline lineage editing via node and column menus (#34551) * feat(lineage): gate lineage editing on permission instead of edit mode Add canEditLineage to the lineage store, computed from hasEditAccess, deleted, platformView, entity type, and scene band instead of the isEditMode toggle. Remove the Edit Lineage button from CustomControls; gate the edge overlay buttons, node connectability, delete-key handler, and drag-drop sidebar on canEditLineage/canEditScene. isEditMode stays in the store but nothing sets it anymore. * refactor(lineage): remove edit mode * feat(lineage): confirm node and edge deletes Add a node-delete confirmation dialog, route node removal (menu and the temporary-node remove button) through it, and make Delete/Backspace open the node or edge confirmation instead of deleting immediately. * refactor(lineage): share edge creation between connect and add-lineage * feat(lineage): add lineage popover Standalone, props-driven AddLineagePopover: step 1 entity type (filtered to TABLE and DASHBOARD_DATA_MODEL when opened from a column, since those are the only search hit shapes getEntityChildrenAndLabel can flatten via its getTableEntityChildren resolver), step 2 entity search excluding the current node, step 3 column picker only when opened from a column. Picking the final value submits immediately (no Add button); placement is left for upstream, right for downstream, no arrow. Task 6 will mount this in LineageMap. * feat(lineage): node menu for adding upstream/downstream and deleting Adds LineageNodeMenu (a per-node dots menu) and mounts it in CustomNodeV1 in place of the already-dead always-there remove button. Wires LineageMap to open the shared AddLineagePopover from the menu's Edit Upstream/Edit Downstream actions and route Delete through the existing node-delete confirmation. Closes the popover on canvas pan/zoom. * feat(lineage): column menu for adding column-level lineage * fix(lineage): keep node menu clear of the drill button SceneDrillButton is absolutely positioned over the same top-right corner as the new node ⋮ menu, so it always painted on top and swallowed the click. Shift the drill button left via a has-lineage-node-menu modifier class whenever the menu renders, and give the menu trigger an explicit 24x24 size so the offset matches its real footprint. * fix(lineage): anchor add-lineage popover to the menu trigger Dropdown.DotsButton is a plain function component with no forwardRef, so ref={triggerRef} placed on it never attached in React 18 and the popover fell back to the viewport origin. Anchor the ref to the wrapper div both LineageNodeMenu and LineageColumnMenu already own instead. * refactor(lineage): remove drag-and-drop node creation * fix(lineage): expose entity search testid on add-lineage popover * feat(lineage): move edge actions into the edge drawer Without edit mode an edge click opens the modal edge drawer, which hid the floating edit/delete toolbar. Editors now get Edit Pipeline / Delete from a menu in the drawer header; the canvas toolbar is removed. Also fixes issues edit mode used to mask: - edge clicks also reached the pane click handler and cleared the selection, so aggregated edges never opened the drawer - a direct load of the Field band lost its column layer to the default layer setting, collapsing column lists and hiding metric column edges - adding a column edge at the Field band always exhausted the scene refetch retries because the confirmation only matched entity edges * test(lineage): drive lineage editing through node and column menus * fix(lineage): address final review of inline lineage editing - Delete key on a selected node matched a drawer copy by reference and never fired; match by entity id and only offer it for removable, non-root nodes - Delete key on a selected edge ignored editability, letting rollup edges reach the delete confirmation; require an editable edge - Dropdown.DotsButton now honours a caller's aria-label, so lineage node, column and edge-drawer menus announce their purpose instead of "Open menu" - The add-lineage popover title names its dialog - Replace suppressed hook deps in NodeChildren with the real dependencies - Read-only lineage checks wait for rendered nodes before asserting no menu * refactor(lineage): drop drag-to-connect node handles Lineage is added from the node and column menus, so the connectable handles on each node side are no longer needed. The expand/collapse buttons keep their handle placement classes. Removes the unused drag-connect path and the dead buildConnectPayload helper. * fix(lineage): mark field-bearing assets expandable without field payloads The asset band fetches no field payloads, so tables, topics, dashboards and other field-bearing kinds were reported as non-expandable and semantic zoom could not drill into them. * feat(lineage): container node cards, control bar and zoom-to-node drill - Container nodes (service/database/schema/domain/data product) render logo + name, a 'Type · kind' subtitle and a child count pill, at a fixed width by node type - Control bar: mini-map toggle state, zoom level, fit to screen replaces fullscreen, aria-labels - Semantic zoom drills only into the node under the pointer and shows a toast when that node has no deeper level - Hover via ReactFlow node events, full-graph loading overlay, ReactFlow as the canvas root with pan-on-focus * test(lineage): use fit to screen and pan helpers in lineage specs * chore(playwright): auto-refresh impact-map.generated.json * style(lineage): replace LESS variables with theme tokens for dark mode - Lineage LESS files now use var(--om-*) tokens instead of variables.less, so node, column, status and panel colours follow the active theme - Add theme-aware --om-color-fg-quaternary, utility-brand-100 and utility-indigo-100/600 tokens to keep light-mode values unchanged - Floating canvas panels (rail, breadcrumbs, layers, skeleton, onboarding) use bg-surface / bg-overlay-surface instead of the page colour - Replace raw tw palette classes in PortsLineageView - Drop rules left dead by the removed edit mode and sidebar * fix(lineage): keep the node menu clear of the scene drill button The drill-button padding on .label-container pushed the ⋮ menu under the absolutely positioned drill button on drillable editable nodes, so the menu could not be clicked. Reserve the gap before the menu instead. Also point the band-selection test at the layer button; the .lineage-map-layer-control class no longer exists. * refactor(lineage): migrate lineage components from AntD to core components Replace antd/@ant-design usage in the lineage UI with @openmetadata/ui-core-components: - LineageConfigModal: core Modal + react-hook-form number inputs - AddPipeLineModal: core Modal, Input, Button - LineageSearchSelect: Select.ComboBox with load-more paging - NodeChildren, CustomNode.utils, TestSuiteSummaryWidget, CanvasButtonPopover, EntitySuggestionOption, LineageTabContent: core Input, Typography, Badge, Button and Tailwind skeletons Drop LESS rules that only targeted the removed .ant-* markup. * chore(playwright): auto-refresh impact-map.generated.json * chore(ui): restore reactColumnResize mock touched by an accidental lint run * style(lineage): read theme colours from globals.css instead of --om-* aliases Lineage LESS, the canvas edge buttons and PortsLineageView now use the --tw-* colour variables generated from core globals.css, which carry the light value and flip under .dark-mode. Drop the four --om-color tokens this branch had added to tokens.css. * docs(ui): take theme colours from globals.css, not --om-color-* Point the styling rule and specs README at the --tw-* variables generated from core globals.css for colours, and note that raw --color-* is invalid in light mode. Non-colour values keep using tokens.css. * refactor(lineage): address review feedback on the inline-editing PR - DotsButton forwards its ref, so LineageNodeMenu drops its wrapper div and anchors the add-lineage popover to the button itself; also drop the English fallback from its aria-label - AddLineagePopover: surface search failures with a toast and cancel a pending search when the entity type changes - Move the pipeline-status badge colours, lineage config fields, form types and form-value helper to constants / interface / utils - Use Typography's color prop, Box for the node label row, and Badge's default colour - Drop the column-label block display that stopped Typography's ellipsis * refactor(lineage): drop the fullscreen z-index and modal z-index overrides Fullscreen lineage no longer raises itself to the sticky layer, so the core ModalOverlay stacks above it on its own and the lineage modals no longer need inline zIndex overrides. * feat(lineage): keep scene navigation on the main Lineage page - The lineage map onboarding dialog only opens on the main Lineage page - Add Lineage to the AI-mode sidebar, pointing at the main Lineage page - Asset pages stay on their own asset: no semantic zoom band changes, band rail, hierarchy breadcrumbs, node drilling, Layer band or lens. A Layer-band URL on an asset page falls back to the Asset band. Asset and Field (column) levels stay available from the Layers menu - Move the Layer-band Playwright checks to the main Lineage page and assert asset pages no longer drill * chore(playwright): auto-refresh impact-map.generated.json * chore(playwright): auto-refresh impact-map.generated.json * chore(playwright): auto-refresh impact-map.generated.json * feat(lineage): core-component Layers menu, column toggle on asset tabs, truncation pill - LineageLayers: rebuild the Layers menu on core Dropdown; the platform page picks a lens (Service/Domain/Data Product), asset tabs get a single "Column Level" toggle - LineageMap: replace the "+N more" badge and the untranslated knowledge-graph-truncated alert with a "Showing X of Y assets" pill - PlatformLineage: search via DataAssetAsyncSelectList; drop EntitySuggestionOption - useAsyncDataAssetOptions: escape ES reserved characters, accept multiple search indexes * feat(lineage): keep the scene map on the main Lineage page only Asset Lineage tabs go back to the classic lineage graph end to end: data comes from getLineage through the <Lineage /> provider and is drawn by the new EntityLineageCanvas (the pre-scene ReactFlow renderer with the canvas edge layer), so asset tabs never call /lineage/scene or carry scene URL params. The PR's inline editing (node menu, add-lineage popover, column menu, delete confirmation) is wired to the classic graph. - Lineage.component: LineageMap for platform lineage, EntityLineageCanvas for asset tabs - Lineage provider: skip the classic fetch on the platform page, restore the ReactFlow instance wiring and expose onInitReactFlow, onNodesChange and refetchLineage - LineageLayers: platform lens menu; asset tabs get the classic layers (column, observability, service, domain, data product) in the same dropdown design - saveLineageEdge shared by the scene map and the asset canvas * test(lineage): move asset-tab Playwright coverage back to the classic graph Asset Lineage tabs load getLineage again, so the asset specs and helpers wait on /lineage/getLineage instead of /lineage/scene and drive the classic layer menu instead of scene bands. Platform-page specs keep the scene. - helpers: activateColumnLayer uses the Column layer, visitLineageTab, column-lineage reloads and connectEdgeBetweenNodes wait on getLineage, deleteEdge no longer waits for a scene refetch; config helpers accept either endpoint; checkLineageTabActions checks the node menu now that edit mode is gone - restore the pre-scene coverage the scene map had replaced: incident counts in the node drawer, the expand/collapse handles and column-layer check in LineageSettings, the entity panel's custom-properties tab per asset type, node pagination, PNG export size, and pipeline annotations read from getLineage - LineageInteraction: band tests become column-layer tests; drop the scene-only hover path highlight, asset-scene persistence and rollup edge tests, which have no asset-tab equivalent - CustomControls: show the lineage time filter on the Lineage tab again, since the classic graph honours it * chore(playwright): auto-refresh impact-map.generated.json * fix(lineage): node menu in the label, expand handles, edge joins, config modal - LineageNodeLabelV1: render the node ⋮ menu inside the label header so the label keeps the node's full width; long names fill it and truncate with an ellipsis - custom-node.less: only drill-button scene nodes position their content box, so the expand/collapse handles are no longer clipped by the node's overflow - CustomNode.utils: the collapse handle is a core Button whose tw:relative beat the legacy absolute class; position it with tw:absolute! so it sits on the node edge - CanvasUtils: end node-level edges at the node border now that the connection handle that filled the 10px gap is gone - LineageConfigModal: move the form onto the core HookForm and actually invoke the submit handler from the OK button - CustomControls: core icons instead of the legacy SVG imports - LineageMap: show asset field counts in scene node footers, and keep the status panel on the platform page * chore(playwright): auto-refresh impact-map.generated.json * fix(lineage): node card feedback, platform rail and edge anchors - Base node: light brand card with brand border; no header/footer or column dividers; dataflow icon for the column-lineage filter - Drill button: gray background and icon; breadcrumb pill without shadow - Platform rail: clickable dots and labels, Layer/Asset/Field stops - Edge anchors: no target inset for scene nodes; temp node polish and breadcrumb-free header - Core Breadcrumbs: title on truncated items - Onboarding subtitle copy updated in all locales * fix(lineage): column search falls back to all columns and survives filter toggle * fix(lineage): add-lineage panel, hover handles, fullscreen bounds, search loader - Add upstream / downstream opens a docked side panel with an edge preview, a short description and an explicit Add step instead of a node popover - Asset lineage handles and expand / collapse controls show on hover, focus or selection; edges end at the node border - Rollup edges keep the base stroke; the count badge is a gray pill - Fullscreen lineage pins to the measured page content column, so it aligns in classic and AI mode with the sidebar expanded or collapsed - Core Autocomplete gains isLoading; the platform search shows a loading state instead of "No data found" while results load * fix(lineage): semantic zoom by screen fill instead of fixed zoom levels Zooming out goes up a level once the whole graph fills under a third of the screen, or the zoom reaches its minimum; zooming in goes down a level once a card fills most of the screen, or the zoom reaches its maximum. Fit to screen may now zoom out as far as the graph needs. * fix(lineage): keep the level rail clear of the minimap; edge actions on asset tabs - The level rail stays centred but never drops into the minimap and zoom controls in the bottom-right corner on short viewports - Edges clicked on an editable asset lineage tab carry isEditable, so the edge drawer offers Edit Pipeline and Delete again * fix(lineage): asset lineage layers read Data Asset Level instead of None * fix(lineage): add-lineage panel reuses the data asset inputs; fit view on asset tabs - Type picker is the core FilterSelect with entity icons, asset counts and search, as on Explore; the asset search is DataAssetAsyncSelectList - Fit to screen on asset lineage tabs now reaches the ReactFlow instance, and fit padding is a viewport ratio (0.2) instead of 48 * fix(lineage): asset tab parity for node drawer, Delete key and column picks - Asset lineage node names open the node drawer, as on the platform map - Delete / Backspace removes the selected node or edge on asset tabs too - The add-lineage panel loads the picked asset's columns from its search hit - Playwright add-lineage helper waits for the FQN option, not a search URL * fix(lineage): inset the layers control from the canvas edge; regular label weight * fix(lineage): review fixes — view-only platform map, safer delete, temp nodes - Platform lineage is view and export only: no node/column menus, add panel, node/edge delete or Delete key on the map; edge drawer is read-only there - Delete / Backspace only acts with focus on the page or the canvas, never from the drawer, a modal or a menu; a pane click clears the selected edge - Temp-table nodes are plain: no menu, not deletable, read-only edges - Deleting a node with several edges no longer keeps removed edges in state - The add-lineage column picker loads columns by FQN - Saving lineage settings keeps the rest of the config (pipeline view mode) - The rail's Data Assets stop works from the Field level - The platform map no longer recentres on focus coming from overlays * chore(playwright): auto-refresh impact-map.generated.json * fix(lineage): CI — core sidebar icon, DotsButton label fallback, keep getColumnHandle - The AI-mode Lineage module uses the core Dataflow03 icon instead of a new raw SVG import (Antd + Less + SVG deprecation guard) - Dropdown.DotsButton keeps the "Open menu" default label, so screens that query the button by name keep working - Restore the getColumnHandle export openmetadata-collate builds against * test(playwright): refresh ai-explore baseline for the AI-mode Lineage sidebar entry * fix(lineage): keep NodeSuggestions and LineageNodeRemoveButton for Collate openmetadata-collate's ER diagram imports both, so the Collate UI build failed once this branch removed them. Restored unchanged from main, with the NodeSuggestions antd suppression (pinned count 496). * fix(lineage): one toast per failed add; resync after a failed node delete - saveLineageEdge resolves false when addLineageHandler fails (it already toasted), so the asset canvas no longer shows the same error twice - A failed node delete is caught and the lineage refetched, so a partial delete no longer leaves the canvas half-updated or rejects unhandled * chore(lineage): address SonarCloud findings - Select / ComboBox: use value + onChange instead of the deprecated selectedKey + onSelectionChange - Drop the unused isConnectable prop from ColumnContent, VirtualColumnList and NodeChildren - LineageColumnMenu: anchor the popover to the DotsButton (it forwards refs) and drop the role="presentation" wrapper; react-aria already stops the press from reaching the column row - Narrow LineageNodeLabelV1 sub-component prop types, Set for COLUMN_CAPABLE_SEARCH_INDEXES, a ZoomGestureEvent alias, and remove a redundant fragment and undefined argument * fix(lineage): fetch asset lineage once per graph; drop superseded responses Lineage settings load after mount and re-apply an equal config, which re-ran the fetch effect while the first request was still in flight: the dedupe key was only written once a response landed. The asset canvas then fetched the same graph twice, and when the second response arrived after the user had switched to the Column layer it redrew with the layers captured at its call, dropping the column rows and edges. Claim the key when the request starts, and ignore a response once a newer request has been issued so the skeleton stays up until the latest graph lands. * test(playwright): open lineage menus by hovering the ⋮ button, not the node centre The middle of a large node can sit under the canvas minimap, which then intercepts the hover until the test times out. * test(playwright): align PlatformLineage specs with the classic asset canvas - Asset-tab Service / Domain views refetch getLineage for the asset's service or domain (lineage-layer-service-btn / -domain-btn) instead of scene lenses, and nothing is selected by default - Dismiss the map onboarding on the first /lineage visit: the asset tab no longer shows it - The Lineage Config dialog's confirm button reads "Ok" (label.ok) now that it is a core Dialog * chore(playwright): auto-refresh impact-map.generated.json * refactor(lineage): core Box, Typography, Card and XClose per review - Edge drawer close button uses XClose - Pipeline picker rows, lineage search options and the map rail use Box for layout and Typography for text (breadcrumb on the text-xs ramp instead of a raw 10px size) - Add-lineage preview cards use Card (brand for the current asset, dashed secondary for the pending one) so both themes come from the component * style(lineage): node borders fall back to --tw-border-color-primary Replaces the legacy #b1b1b7 token with the globals.css border token, per review; dark mode still overrides through --lineage-node-border. * chore(playwright): auto-refresh impact-map.generated.json * test(playwright): fix specs broken by shared lineage-branch changes - ImpactAnalysis: match "Asset Level" exactly; the Layers trigger now reads "Data Asset Level" - ContextCenterMemories: the asset search escapes Elasticsearch-reserved characters, so compare the unescaped query - MetricBulkImportExportEdit: Dropdown.DotsButton keeps the caller's aria-label, so the metrics menu is named "Actions", not "Open menu" - LineageInteraction: fire the column click in place; the minimap can cover the column after fit - PlatformLineage: pick a depth different from the current one; the global lineage settings are shared with other specs --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * Fixes #32000, #33553: Hand confidential OIDC browsers the identity provider's ID token (#34967) * Mint the configured emailClaim into OpenMetadata-issued tokens The email-first flow reads a token's address from `emailClaim` alone. Our own tokens (SAML, LDAP and basic sessions, personal access tokens, and the confidential OIDC session until it hands out the provider's token) never carried that claim, so with `emailClaim: upn` JwtFilter fell back to the legacy claim order and resolved the email's local part (`john.doe` for user `jdoe`), or failed outright once `jwtPrincipalClaims` was removed as the deprecation warning suggests. Same class of bug as #32000, through the email-first flow instead of `jwtPrincipalClaimsMapping`. Without a mapping, the configured emailClaim now carries the email, the way the provider's own token does. * Validate the issuer and audience of identity-provider tokens JwtFilter checked a provider token's signature and expiry. For tokens not signed with OpenMetadata's own key it now also requires: - an audience that includes a configured client: the browser client (`clientId`) or the server-side client (`oidcConfiguration.id`); - for confidential clients, the issuer named in the provider's discovery document (a multi-tenant Entra template is filled in from the token's `tid`; Google's schemeless issuer is accepted). The provider's `jwks_uri` from discovery is trusted alongside `publicKeyUrls`. The SSO settings save writes it there, but a deployment configured only through the environment never had it. The confidential sign-in handler registers after JwtFilter at startup and is replaced on a configuration change, so both values are looked up per use, and only when the handler serves this filter's client. Bot tokens and personal access tokens are recognised by OpenMetadata's signing key id, which key lookup resolves locally first, so tokens minted under an earlier issuer keep working. The integration-test harness now presents the provider's ID token as its API bearer, as the browser does: the mock IdP's access token carries no audience. * Hand confidential OIDC browsers the identity provider's ID token Since 2.0 the confidential OIDC callback minted an OpenMetadata JWT for the browser. JwtFilter then re-resolved identity from that copy with the provider-oriented claim configuration, so any claim the copy did not carry broke login (#32000), and its lifetime came from `oidcConfiguration.tokenValidity` rather than the provider (#33553). OpenMetadata now issues tokens only where the protocol gives the browser none to present: basic, LDAP and SAML. - Callback: the browser receives the provider's ID token from the code exchange. The provider's refresh token is always kept, encrypted, in the session. - Refresh: every refresh redeems that refresh token for the provider's next ID token and returns it with its expiry. A rejected grant, a session holding no provider refresh token, or a renewal without an ID token that outlives the browser's one-minute renewal margin ends the session with a 401, which the browser answers by signing in again at the provider. A provider that gives no verdict gets a 503; the session is kept for a later refresh. - Login no longer short-cuts an active session with a minted token; the provider's own session makes the round trip silent. - `oidcConfiguration.tokenValidity` and `endSessionWithProvider` are deprecated and ignored for OIDC; the SSO form no longer shows them. `sessionExpiry` still caps the session, and `maxAge` is still sent to the provider. SAML keeps its `tokenValidity`. The provider's ID token carries no `sessionId`, so logout, revocation and the per-user session cap take effect at the browser's next refresh, as in the public OIDC flow. SSOSessionLimit.spec.ts now asserts that. * Check the refresh-token flow in an SSO Test Login A confidential OIDC session now renews the browser's ID token by redeeming the provider's refresh token on every refresh. A provider that issues no refresh token, refuses its own, or renews without an ID token would send every user back to sign in each time their ID token expires, and nothing told the admin beforehand. Test Login gains a TOKEN_REFRESHED stage, after TOKEN_VALIDATED. It redeems the test sign-in's refresh token once, with the same grant, client authentication and token endpoint the live refresh uses, and fails with the reason and the fix: request offline_access (Okta, Microsoft Entra ID, Auth0), allow refresh tokens for the client, or for Google the consent screen. The identity is still resolved when the stage fails, and a failed Test Login gates Save as before ("Save anyway" remains the override). The stage is skipped for public OIDC clients, whose browser renews its own tokens, and for SAML, LDAP and basic. The SSO settings label is translated into every locale. * Cover the unreadable ID token, refused key set and unanswered Test Login refresh * Revoke the session a new sign-in replaces A sign-in from a browser that already has a session writes the new pending session's cookie over the old one. The old session stayed active until it expired: nothing could reach it any more, but it still counted toward maxActiveSessionsPerUser, so signing in again on one browser could evict the user's session on another device. Login now revokes the active session the browser's cookie names before creating the pending session, on the MCP flow as well, since that flow replaces the cookie too. The user's other sessions stay active. * Chore: Remove Delta Lake connector (#35299) * chore(ingestion): remove the Delta Lake connector source Drop the DeltaLake database source package, its example workflow, unit and integration tests, the deltalake/deltalake-storage/deltalake-spark extras and the stale lint-baseline entries. Delta Lake as a table format (Trino and StarRocks detection, _delta_log parsing in Datalake/storage) is untouched. Amundsen's "delta" databases now map to a CustomDatabase service, matching how the upgrade migration retypes existing DeltaLake services. * chore(spec): remove the DeltaLake connection schemas Delete deltaLakeConnection.json and its deltalake/metastoreConfig.json and deltalake/storageConfig.json sub-schemas, and drop DeltaLake from the databaseServiceType enum and the database connection oneOf. TableType.DeltaLake stays: it describes the table format, not the connector. UI types and bundled JSON schemas regenerated with json2ts-generate-all.sh and yarn parse-schema; both produce no diff on the unchanged schemas. * chore(service): remove the DeltaLake secret converters and test-connection definition Drop DeltaLakeConnectionClassConverter and StorageConfigClassConverter (the latter only converted the deltalake StorageConfig), their ClassConverterFactory entries, the DeltaLake test-connection definition, and DeltaLake from the ODCS backtick-quoted service types. NestedSecretConverterCoverageTest no longer allow-lists MetastoreConfig.connection, which is gone with the schema. * chore(ui): remove the Delta Lake service form, icon and docs Drop the DeltaLake connection-schema loader, service icon, display labels and in-app connector doc, and the schema-render test case that rendered the DeltaLake form. The compact-select behaviour for *Source oneOf fields stays covered by CoreOneOfField.test.tsx. Remove deltalake from the connector-building skill's data-lake examples. * fix(migration): retype DeltaLake services on upgrade 'DeltaLake' is no longer a databaseServiceType, so rows that still carry it fail to deserialize: listing database services returns an error, and the 2.1.0 data migration itself aborts in DataContractEntityReferenceMigration when a data contract points at a DeltaLake table. Before that data migration runs, 2.1.0 schemaChanges.sql (MySQL and Postgres) now: - deletes the ingestion pipelines of DeltaLake services together with their version snapshots, run statuses and relationships; a pipeline is tied to its service only by the service's CONTAINS relationship; - rewrites serviceType to CustomDatabase on the services, their databases, schemas, tables and stored procedures, and on their version snapshots; - replaces the service connection config with {"type": "CustomDatabase"}, so the old metastore password and storage keys are not kept as unmasked additional properties; - deletes the DeltaLake test-connection definition and any DeltaLake test-connection workflows. Every statement is keyed on 'DeltaLake', so a second run changes nothing. DeltaLakeServiceRetypeSqlMigrationTest runs the shipped statements against real MySQL and PostgreSQL. * fix(migration): retire DeltaLake services in a Java step that removes their DAGs The 2.1.0 SQL deleted DeltaLake ingestion pipeline rows but cannot reach the pipeline service, so their deployed DAGs stayed scheduled in Airflow. DeltaLakeRetirementMigration now runs first in the v210 data migration on MySQL and Postgres, before any step that loads database entities: - reads the DeltaLake pipeline ids through each service's CONTAINS relationship while the services still carry the old type; - retypes the services, their databases, schemas, tables and stored procedures, and their version snapshots, to CustomDatabase in IdBatches, replacing a service's connection config with {"type": "CustomDatabase"}; - deletes the DeltaLake test-connection definition and workflows; - force-deletes each pipeline through IngestionPipelineRepository with the migration's pipeline service client, which removes its DAG, statuses, relationships and search document; an unreachable runner is logged and its DAG left, as a service cascade delete does. The 2.1.0 schemaChanges.sql files are back to their main content, so ALTER DATABASE is the last MySQL statement again. The rule is recorded in ADR:2026-10-13-a-removed-connector-is-retired-in-the-first-data-migration-step. * chore: drop the DeltaLake retirement decision record * fix(migration): keep retiring DeltaLake services when a pipeline delete fails The pipeline ids are read before the retype, the only point a DeltaLake pipeline can be told apart, and the retype commits first because the delete loads the pipeline's service, which no longer deserializes while it is typed DeltaLake. By the time the deletes run their ids are gone from the database, so a propagated failure stranded every pipeline behind it with no way for a later run to find them again. Log the failure with the pipeline id and carry on, and stop the Javadoc and the decision record claiming the delete resumes like the retypes do. Restores the decision record the enum removal and the migration ordering need, which the earlier SQL-to-Java rewrite dropped. * chore(playwright): auto-refresh impact-map.generated.json * fix(migration): resume the DeltaLake pipeline delete after a killed run The pipeline ids cannot be keyed on 'DeltaLake' like every other statement in this step: they are read before the retype, the only point a DeltaLake pipeline can be told apart, and the handle autocommits, so the retype has landed by the time the deletes run. A run killed inside the delete loop therefore left its remaining pipelines and their deployed DAGs behind for good, because the next run's query finds no DeltaLake service to name them. Write each id to entity_extension as deltaLakeRetirement.pendingPipeline before the retype and clear it once its delete has been attempted, so a killed run finds its leftovers on the next start. The marker is keyed on the pipeline's own id, so a hard-deleted pipeline takes it along. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * Fixes #34689: record every Assets tab edit on the asset as its own PATCH (#35026) * feat(entities): apply a bulk edit as one authorized PATCH per entity * fix(entities): index a patched entity with the fields it inherits * fix(assets): save every Assets tab edit as the asset's own PATCH * fix(search): keep a table's tags and data products off its column entries * docs(decisions): record how bulk edits write, authorize and index * fix(entities): fail only the group that cannot be loaded or recorded * fix(assets): refuse an asset whose labels clash with its glossary's tags on the glossary term tab * refactor(assets): derive the Assets tab status with the shared bulk status rule * test(assets): drop the fail-before-pass gate test that per-asset saves replace * test(assets): compare tab and PATCH labels without their appliedAt * test(assets): keep the search entry checks clear of the response cache and of unordered updates * test(assets): check the column entries of all tables in one search per poll * test(assets): drop the domain move check that waits on main's lossy column propagation * fix(assets): authorize the tag tab job with the request's impersonation * fix(ui): report assets the server refuses to remove from an Assets tab * chore(playwright): auto-refresh impact-map.generated.json * fix(ui): ask to refresh when a tag tab removal's outcome does not arrive * test(glossary): expect a refused bulk asset row instead of a 403 * fix(search): send column entries the glossary terms they inherit from their table --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * Fixes #34249: Add Neo4j database connector (#34250) * feat(ingestion): Add Neo4j database connector Ingest a Neo4j graph schema as a database service: node labels become tables in a `nodes` schema and relationship types become tables in a `relationships` schema, with their properties as columns typed from db.schema.nodeTypeProperties() / db.schema.relTypeProperties(). - Connection schema (scheme, hostPort, username, password, databaseName, includeRelationships, propertyFilterPattern and the standard filters) and a test connection definition (CheckAccess, GetDatabases, GetNodeLabels, GetRelationshipTypes) - Source built on CommonNoSQLSource with declared property types, the multi-label NOT NULL rule, a property filter, and a failed schema read skipping only that schema - BaseConnection with test-connection checks and an error pack for the neo4j driver's status codes - `neo4j` pip extra, CLI workflow example, unit tests - UI: connection form schema, service icon, side-panel docs, Beta flag and regenerated TypeScript types * fix(ingestion): read the Neo4j graph schema once and keep failed schemas out of deletion The post-process deletion hooks list schemas again through get_schema_name_list(), which re-queried Neo4j. A schema whose read failed during the walk could then reappear and have all its tables soft-deleted, a persistently failing schema was soft-deleted when markDeletedSchemas is on, and each database ran the schema procedures up to four times. Cache the graph schema per database, record which schemas failed, and add failed schemas to the schema source state before the schema deletion sweep. * Address Neo4j connector review feedback - Silence driver notifications so schema reads don't log a deprecation warning each run - Reuse the shared neo4j pin (now ~=5.7, needed for notifications_min_severity) - Fix the GetNodeLabels / GetRelationshipTypes test-connection descriptions - Drop the unused databaseFilterPattern from the connection schema - Note the upcoming propertyTypes format change next to the schema queries * Fix py-checkstyle and regenerate UI types for the Neo4j connector - Use lazy %-style logging for the filtered-property debug message (G004) - Regenerate TS types and dereferenced schemas with json2ts-generate-all.sh and parse-schema --------- Co-authored-by: michael gong <209566851+michaelgong428@users.noreply.github.com> Co-authored-by: Khairajani <himanshukhairajani8@gmail.com> * fix(lineage): strip FQN quotes from schema/db/table names before applying view filters (#34365) (#34568) * fix(lineage): strip FQN quotes from schema/db/table names before applying view filters Fixes #34365 `yield_es_view_def` splits each view's `fullyQualifiedName` with `fqn.split()`, which preserves double-quoted identifiers verbatim. A schema named `folder.sub` (containing a dot) becomes `'"folder.sub"'` in the split result. `view_lineage_producer` passed that quoted string directly to `filter_by_schema`, so a pattern like `^folder\.sub$` never matched and every view in the schema was reported as *View Filtered Out* (includes filter) or passed through unchecked (excludes filter). The fix calls `fqn.unquote_name()` on all three name fields before passing them to the filter helpers. `unquote_name` strips outer double-quotes only when they are present, so plain names are unaffected. Two unit tests added: - Verifies that an includes-pattern on the unquoted schema name keeps the view. - Verifies that an excludes-pattern on the unquoted schema name filters it out. - Existing view-lineage-producer tests continue to pass. Prepared with AI assistance (Claude Code, Anthropic), reviewed for correctness before submission. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ingestion): pass Python checkstyle for view lineage PR --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Harsh Soni <64592571+harshsoni2024@users.noreply.github.com> Co-authored-by: harshsoni2024 <harshsoni2024@gmail.com> * Fix TagPage test mock, refresh ai-explore baseline, record Classic deprecation - TagPage.test mocks @openmetadata/ui-core-components without Badge, which TabsLabel's count now renders; add it from the real module. - ai-explore visual baseline predates the Lineage nav item #34551 added on main (its own merge-queue run failed the same snapshot). The new baseline is CI's actual render from the pinned Playwright container, pixel-identical with and without this PR. - Add the decision record for the Classic mode deprecation notice. * chore(playwright): auto-refresh impact-map.generated.json * Update generated TypeScript types and dereferenced schemas * Reset bot-generated schema JSON and impact map to main They were generated from the branch before the replayed main commits were dropped; this PR changes no schemas, and CI regenerates the impact map. * chore(playwright): auto-refresh impact-map.generated.json --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Rohit Jain <60229265+Rohit0301@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Teddy <teddy.crepineau@gmail.com> Co-authored-by: Anujkumar Yadav <anujf0510@gmail.com> Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Co-authored-by: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Co-authored-by: Akash Verma <138790903+akashverma0786@users.noreply.github.com> Co-authored-by: Adrià Manero <adria.estivill@getcollate.io> Co-authored-by: michaelgong428 <michael.gong@finsure.com.au> Co-authored-by: michael gong <209566851+michaelgong428@users.noreply.github.com> Co-authored-by: Khairajani <himanshukhairajani8@gmail.com> Co-authored-by: zerafachris <christopher.zerafa@blocklabs.io> Co-authored-by: Harsh Soni <64592571+harshsoni2024@users.noreply.github.com> Co-authored-by: harshsoni2024 <harshsoni2024@gmail.com>



Fixes #34689
Problem
Adding or removing a tag, glossary term, domain or data product from an Assets tab changed the assets but recorded nothing on them: no new version, no "updated by", no change event for the activity feed, alerts or audit log. The tab endpoints wrote relationship rows directly instead of saving the asset.
PR #34690 did this through a second save pipeline. This PR starts over so an entity has one way to be saved.
What this PR changes
An Assets tab edit is now exactly the PATCH the user could make on each asset's page.
EntityPatchBatchtakes the selected assets in groups of 100 and loads each group in one query. For each asset it applies the edit to a copy, turns the difference into a JSON patch, authorizes the asset as that PATCH, and saves it through theEntityRepository.patchcode a single PATCH runs, with the same rules and versioning. A selected column is saved through its table. An asset an approval workflow gates is held as that PATCH would be, and reported as pending with its change request. The tag tab's background job keeps the request's impersonation, soisImpersonated()rules apply there too.Two related fixes:
The principles are recorded in
docs/decisions/.API change
The tag and glossary term
assets/addandassets/removeendpoints no longer answer 403 for the whole request. An asset the caller may not edit comes back as a failed row with the reason, in a 200 whosestatusisfailureorpartialSuccess(for tags, in the job's outcome). Clients must readstatus, not only the HTTP code. Domain and data product endpoints still answer 403 to a caller who may not edit that domain or data product.What users will notice
How it was tested
New
AssetsTabVersioningIT(versions, events, columns, partial failure, 251 assets, parity with the PATCH),AssetsTabAuthorizationIT(owner-only and deny rules, a domain editor, an impersonating bot) andAssetsTabSearchEntriesIT, plus Jest tests for refused removals. With the related suites, approval holds included, 1,218 tests pass on MySQL; the most affected also on Postgres.Median of 5 rounds: a glossary term on 100 tables takes 2.7 s (3.4 s on main).
Not in this PR
Sending each group's search writes as one bulk request (#35023).
The cache must have an explicit size cap before merging to satisfy the repository’s requirement.
Findings
Summary
Assets tab edits now use each asset’s normal PATCH path.
Acknowledged limitations:
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Assets tab edit] --> B[Resolve selected assets and columns] B --> C[Group by saved entity] C --> D[Load up to 100 entities] D --> E[Apply edit to a copy and build PATCH] E --> F[Check permission for each asset] F -->|Refused| G[Failed row] F -->|Allowed| H[Normal entity PATCH] H -->|Approval required| I[Pending change request] H -->|Saved| J[Version, search, and change event] G --> K[Bulk result] I --> K J --> KReviews (1) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile