Skip to content

feat: add customizable Data Marketplace landing page - #26255

Merged
siddhant1 merged 104 commits into
mainfrom
feat/data-marketplace-page
Apr 6, 2026
Merged

feat: add customizable Data Marketplace landing page#26255
siddhant1 merged 104 commits into
mainfrom
feat/data-marketplace-page

Conversation

@siddhant1

@siddhant1 siddhant1 commented Mar 5, 2026

Copy link
Copy Markdown
Member

Describe your changes:

I worked on adding a new Data Marketplace page at /data-marketplace — a discovery hub for data products and domains, because the platform needed a centralized entry point for browsing and discovering data products and domains with persona-based customization support.

Core features:

  • Greeting banner, search bar with recent searches (per-user Zustand store), and widget cards for recently created data products and domains
  • ReactGridLayout with per-persona layout customization via DocStore (same pattern as MyData home page)
  • DataMarketplace added to the PageType backend schema enum
  • "Marketplace" section in the left sidebar with links to marketplace, data products, and domains
  • Edit mode accessible via Settings → Persona → Data Marketplace, or the customize button on the page

New files:

  • pages/DataMarketplacePage/ — Main page component + styles
  • components/DataMarketplace/ — MarketplaceGreetingBanner, MarketplaceSearchBar, MarketplaceDataProductsWidget, MarketplaceDomainsWidget, MarketplaceItemCard, AnnouncementsWidgetV2
  • hooks/useMarketplaceRecentSearches.ts — Recent search persistence hook
  • hooks/useMarketplaceStore.ts — Zustand store for marketplace navigation context (uses ROUTES constants)

Modified files:

  • Backend schema: page.json — new DataMarketplace PageType
  • Generated enums: page.ts, uiCustomization.ts
  • CustomizeMyDataPageClassBase — marketplace widget registrations + default layout
  • CustomizeMyData — dynamic pageType + default layout (no longer hardcoded to LandingPage)
  • CustomizablePageHeader — "Add Widgets" button visible for marketplace pages
  • CustomizablePage — DataMarketplace routing case
  • PersonaUtils — DataMarketplace category in persona settings
  • Route, sidebar, i18n additions

Code quality improvements (reviewer feedback):

  • AnnouncementsWidgetV2: Use Typography size/weight props instead of custom Tailwind font-size/weight classes
  • MarketplaceItemCard: Use isClickable prop on Card component; Space key now also activates the card for accessibility
  • marketplace-widget-shared.less: Replaced hardcoded hex colors (#fff, #eaecf0) with Less variables (@white, @border-color)
  • useMarketplaceStore: Now imports and uses ROUTES constants instead of hardcoded path strings to avoid drift
  • useMarketplaceRecentSearches: Fixed in-place mutation of Zustand state ([...entries].sort() instead of entries.sort())
  • useCallback dependency arrays fixed in MarketplaceDomainsWidget, MarketplaceDataProductsWidget, and MarketplaceSearchBar
  • Greeting banner emoji (👋) moved into i18n translation strings across all locale files
Screen.Recording.2026-03-17.at.10.56.12.AM.mov

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.
  • The issue properly describes why the new feature is needed, what's the goal, and how we are building it. Any discussion or decision-making process is reflected in the issue.
  • I have updated the documentation.
  • I have added tests around the new logic.

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Add a new Data Marketplace page at /data-marketplace with:
- Greeting banner, search bar with recent searches (per-user via Zustand)
- New Data Products and New Domains widgets with add/view-all actions
- ReactGridLayout-based customizable widget grid
- Per-persona layout persistence via DocStore (same pattern as MyData home page)
- New DataMarketplace PageType in backend schema
- Marketplace section in left sidebar
- Edit mode via /customize-page/:personaFqn/DataMarketplace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Comment thread openmetadata-ui/src/main/resources/ui/src/utils/CustomizeMyDataPageClassBase.ts Outdated
Changed marketplaceDataProducts and marketplaceDomains default heights
from 3 to 1 to match the page's MARKETPLACE_ROW_HEIGHT of 200px.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Replace the landing page customization pattern (CustomizeMyData) with
the entity detail page pattern (CustomizeTabWidget) for the Data
Marketplace page. This gives proper drag handles, add/remove controls,
and tab-based widget management.

- Create DataMarketplaceClassBase with default tabs, layouts, widgets
- Create CustomizableDataMarketplacePage using CustomizeTabWidget
- Register DataMarketplace in CustomizePageUtils (all 6 switch statements)
- Add marketplace widgets to GenericWidgetUtils WIDGET_COMPONENTS
- Update DataMarketplacePage to use tab-based layout (8-col grid)
- Clean up old landing page pattern code from CustomizeMyDataPageClassBase
- Move widget keys from LandingPageWidgetKeys to DetailPageWidgetKeys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 3 comments.

Comment thread openmetadata-ui/src/main/resources/ui/src/App.tsx
Siddhant and others added 2 commits April 3, 2026 19:33
…Name

Commit 119e9fe accidentally reverted the fix from PR #26793 by
changing entity.name back to getEntityName(entity) for Metric
breadcrumbs. In search results, getEntityName() returns the
displayName which contains Elasticsearch highlight HTML fragments
(<span class="text-highlighter">), causing breadcrumbs to render
raw HTML as text. Using entity.name directly returns the plain,
non-highlighted name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 2 comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 3 comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 3 comments.

Comment on lines +269 to +276
<SelectPopover
isNonModal
className="!tw:max-h-[400px]"
isOpen={isOpen && searchValue?.trim().length > 0}
offset={4}
placement="bottom"
size="md"
style={{ width: containerRef?.current?.offsetWidth }}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

SelectPopover uses className="!tw:max-h-[400px]", which doesn’t match the repo’s Tailwind prefixing pattern (e.g., tw:!size-4). As written, this class likely won’t be generated/applied, so the popover may not get the intended max-height styling. Use the prefixed important syntax (e.g., tw:!max-h-[400px]) or move the constraint into the .marketplace-search-results styling only.

Copilot uses AI. Check for mistakes.

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 1 comment.

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 2 comments.

Comment thread openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
Comment on lines 140 to 154
export const getDomainPath = (fqn?: string) => {
let path = ROUTES.DOMAIN;
const basePath = useMarketplaceStore.getState().domainBasePath;

if (fqn) {
path = ROUTES.DOMAIN_DETAILS;
path = path.replace(PLACEHOLDER_ROUTE_FQN, getEncodedFqn(fqn));
return `${basePath}/${getEncodedFqn(fqn)}`;
}

return path;
return basePath;
};

export const getDomainDetailsPath = (
fqn: string,
tab?: string,
subTab = 'all'
) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Performance: getCertificationClassification called on every getTags invocation

Every call to getTags(fqn) now calls getCertificationClassification(), which reads from SettingsCache. When listing entities (e.g., paginated list of 100 tables with tags), this results in 100+ cache lookups per request. While SettingsCache is in-memory, the repeated deserialization of AssetCertificationSettings and string allocation could add up in hot paths. Consider caching the classification name in a field (invalidated on settings change) or computing it once per request.

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

Comment on lines +366 to +370
const handleLanguageChange = useCallback(({ key }: MenuInfo) => {
i18next.changeLanguage(key);
setPreference({ language: key as SupportedLocales });
navigate(0);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Bug: useEffect missing deps: currentPage, handlePagingChange

The pagination sync useEffect at line 137 uses handlePagingChange, currentPage, and handlePageChange in its body, but the dependency array is [allTasksInternal.length, pageSize]. While handlePagingChange (which is setPaging) is stable, currentPage is read directly and handlePageChange is recreated when currentPage changes. This means the currentPage > maxPage correction (line 140) won't re-run when currentPage changes externally (e.g. via URL params). The eslint exhaustive-deps rule would flag this.

Suggested fix:

useEffect(() => {
  handlePagingChange({ total: allTasksInternal.length });
  const maxPage = Math.max(1, Math.ceil(allTasksInternal.length / pageSize));
  if (currentPage > maxPage) {
    handlePageChange(maxPage, { cursorType: null, cursorValue: undefined });
  }
}, [allTasksInternal.length, pageSize, currentPage, handlePagingChange, handlePageChange]);

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

@gitar-bot

gitar-bot Bot commented Apr 6, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 29 resolved / 38 findings

Adds customizable Data Marketplace landing page with 13 open issues including critical findings: MarketplaceNavBar duplicates 500+ lines from NavBar, missing cleanup of polling setTimeout on unmount, showErrorToast firing on expected 404s, and global domain/data-product path functions coupled to mutable store state. Address these blockers before approval.

⚠️ Quality: MarketplaceNavBar is a ~500-line copy-paste of NavBar

📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:76 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:159 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:76-90 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:97-111 📄 openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx

MarketplaceNavBar is almost entirely duplicated from NavBar.tsx (~500 lines), including all websocket handling, notification logic, version-mismatch alerts, and browser notification code. This duplication means every future bug fix or feature addition to NavBar must be mirrored here, which is a significant maintenance burden. It also inherits the existing handleBellClick stale-closure bug (dependency array [hasTaskNotification] is missing hasMentionNotification and activeTab).

Consider extracting the shared logic (socket listeners, notifications, version check, language switcher, help menu, user profile) into a shared hook or a common NavBarBase component, and composing the two variants from it. MarketplaceNavBar's only unique element is the <MarketplaceNav /> dropdown replacing the search bar.

Suggested fix
Extract shared navbar logic into a custom hook (e.g. useNavBarState) or a NavBarShell component that accepts a `leftContent` slot. MarketplaceNavBar and NavBar would both compose from that shared base, each providing their own left-side content (search bar vs marketplace nav).
⚠️ Bug: entityIconMapping drops SearchIndex.ML_MODEL_SERVICE ('mlModelService')

The refactoring that moved entityIconMapping from inside getEntityIcon() to module scope removed SearchIndex.ML_MODEL_SERVICE (value 'mlModelService'), assuming it was a duplicate of EntityType.MLMODEL_SERVICE (value 'mlmodelService'). However, these are different strings (capital 'M' in 'Model' vs lowercase). Any code path that calls getEntityIcon('mlModelService') — e.g., search results displaying ML model services — will now fall through to the default case and show no icon.

Other removed entries (e.g., SearchIndex.MESSAGING_SERVICE, SearchIndex.STORAGE_SERVICE) happen to have the same string values as their EntityType counterparts and are fine.

Suggested fix
Add the missing entry back to the module-level mapping:

  [SearchIndex.ML_MODEL_SERVICE]: MlModelIcon,

after the existing `[EntityType.MLMODEL_SERVICE]: MlModelIcon,` line.
⚠️ Edge Case: showErrorToast fires on expected 404 when persona has no layout

📄 openmetadata-ui/src/main/resources/ui/src/pages/DataMarketplacePage/DataMarketplacePage.component.tsx:103-105 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/AnnouncementsWidgetV2/AnnouncementsWidgetV2.component.tsx:122-124 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/AnnouncementsWidgetV2/AnnouncementsWidgetV2.component.tsx:68-82 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/AnnouncementsWidgetV2/AnnouncementsWidgetV2.component.tsx:74-79

When a persona has no customization document, getDocumentByFQN returns a 404. The new showErrorToast call in the catch block will display an error notification to the user for this normal, expected condition. Other callers like MyDataPage silently catch this error, and CustomizablePage explicitly checks for 404 status to handle it gracefully.

Users who haven't customized their marketplace page will see an error toast every time they visit.

Suggested fix
} catch (error) {
  if ((error as AxiosError).response?.status !== ClientErrors.NOT_FOUND) {
    showErrorToast(error as AxiosError);
  }
  setLayout(defaultLayout);
}
💡 Edge Case: NPE risk in getTags cert-tag filter if tagFQN is null

In getTags, the removeIf predicate calls FullyQualifiedName.getParentFQN(tag.getTagFQN()). If tag.getTagFQN() returns null (e.g., due to corrupt data or incomplete migration), getParentFQN will call CharStreams.fromString(null) and throw a NullPointerException. While tagFQN is required in the schema, the database may contain legacy rows without it. A null guard would make this defensive.

Suggested fix
tags.removeIf(
    tag -> tag.getTagFQN() != null
        && certClassification.equals(FullyQualifiedName.getParentFQN(tag.getTagFQN())));
💡 Performance: getCertificationClassification called on every getTags invocation

📄 openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts:140-154 📄 openmetadata-ui/src/main/resources/ui/src/hooks/useMarketplaceStore.ts:24-38 📄 openmetadata-ui/src/main/resources/ui/src/pages/DataMarketplacePage/MarketplaceLayout.component.tsx:25-29 📄 openmetadata-ui/src/main/resources/ui/src/hooks/useMarketplaceStore.ts 📄 openmetadata-ui/src/main/resources/ui/src/utils/EntityUtilClassBase.ts:358-364 📄 openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts:169-183 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceDataProductsWidget/MarketplaceDataProductsWidget.component.tsx:150-152 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceSearchBar/MarketplaceSearchBar.component.tsx:128-129 📄 openmetadata-ui/src/main/resources/ui/src/hooks/useMarketplaceStore.ts:26-38 📄 openmetadata-ui/src/main/resources/ui/src/pages/DataMarketplacePage/MarketplaceLayout.component.tsx:25-28

Every call to getTags(fqn) now calls getCertificationClassification(), which reads from SettingsCache. When listing entities (e.g., paginated list of 100 tables with tags), this results in 100+ cache lookups per request. While SettingsCache is in-memory, the repeated deserialization of AssetCertificationSettings and string allocation could add up in hot paths. Consider caching the classification name in a field (invalidated on settings change) or computing it once per request.

💡 Security: AppRunLogAppender path traversal tests missing

The resolveLogFile and resolveAppDir methods in AppRunLogAppender correctly implement path traversal protection via normalize() + startsWith(base). However, the entity name schema (^((?!::).)*$) does not block path components like ../ or /, meaning the runtime check is the sole defense. The test suite (AppRunLogAppenderTest) doesn't include any path traversal test cases to verify this protection continues working.

Suggested fix
Add a test case to AppRunLogAppenderTest:

@Test
void startCapture_rejectsPathTraversal() {
  assertThrows(IllegalArgumentException.class,
    () -> AppRunLogAppender.startCapture(
      "123", "id", "../../etc", "server1"));
}
💡 Security: resolveBaseUrl() falls back to http://localhost:8585

The resolveBaseUrl() method in McpCallbackServlet returns "http://localhost:8585" as a hardcoded fallback (line 124) when both the MCP config and system settings fail to provide a base URL. This value is used to construct the session redirect URI at line 296 (baseUrl + "/mcp/callback"). In production, if configuration loading fails transiently, the OAuth redirect would target localhost instead of the real server, causing the callback to fail silently. While this isn't an open redirect in the traditional sense (the attacker can't control the URL), it could cause confusing auth failures in production and the use of http:// (not https://) for a security-sensitive callback is a concern.

Suggested fix
Consider throwing an IllegalStateException instead of silently falling back:

throw new IllegalStateException(
    "Cannot resolve base URL for MCP callback. "
    + "Configure mcpConfiguration.baseUrl or openMetadataBaseUrl.");
💡 Bug: useEffect missing deps: currentPage, handlePagingChange

📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:366-370

The pagination sync useEffect at line 137 uses handlePagingChange, currentPage, and handlePageChange in its body, but the dependency array is [allTasksInternal.length, pageSize]. While handlePagingChange (which is setPaging) is stable, currentPage is read directly and handlePageChange is recreated when currentPage changes. This means the currentPage > maxPage correction (line 140) won't re-run when currentPage changes externally (e.g. via URL params). The eslint exhaustive-deps rule would flag this.

Suggested fix
useEffect(() => {
  handlePagingChange({ total: allTasksInternal.length });
  const maxPage = Math.max(1, Math.ceil(allTasksInternal.length / pageSize));
  if (currentPage > maxPage) {
    handlePageChange(maxPage, { cursorType: null, cursorValue: undefined });
  }
}, [allTasksInternal.length, pageSize, currentPage, handlePagingChange, handlePageChange]);
💡 Quality: Hardcoded hex colors in 'Load more' badge

The new "Load more" badge uses hardcoded '#ffffff' and '#155EEF' (lines 445-447 of useOntologyGraph.ts) while the adjacent asset-count badge correctly uses NODE_FILL_DEFAULT and NODE_LABEL_FILL constants. This is inconsistent with the PR's own stated goal of replacing hardcoded colors, and makes the badge immune to theming. '#155EEF' (the primary blue) has no named constant exported from the constants file.

Suggested fix
// Extract to constants in OntologyExplorer.constants.ts:
export const DATA_MODE_LOAD_MORE_BG = '#155EEF';
// Then use:
fill: NODE_FILL_DEFAULT,
backgroundFill: DATA_MODE_LOAD_MORE_BG,
✅ 29 resolved
Performance: Marketplace widgets are eagerly imported in base class

📄 openmetadata-ui/src/main/resources/ui/src/utils/CustomizeMyDataPageClassBase.ts:33
CustomizeMyDataPageClassBase.ts statically imports MarketplaceDataProductsWidget and MarketplaceDomainsWidget at the top level. Since this base class is used by the main landing page, these marketplace components and their dependencies will be bundled into the main chunk even for users who never visit the Data Marketplace page. Consider using React.lazy() or dynamic import() in getWidgetFromKey() to code-split these widgets, consistent with how other page-specific widgets could be loaded on demand.

Bug: Debounced search not cancelled on unmount; races with direct fetch

📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceSearchBar/MarketplaceSearchBar.component.tsx:84 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceSearchBar/MarketplaceSearchBar.component.tsx:89
The debouncedFetch created via useMemo is never cancelled when the component unmounts. If the user navigates away while a debounced call is pending, it will fire and attempt setState on an unmounted component.

Additionally, handleSearch (triggered on Enter) calls fetchResults directly without cancelling the pending debounced call. If a user types quickly and presses Enter, the debounced call can resolve after the direct call, overwriting the correct results with stale ones from the earlier debounced query.

Quality: Duplicate LESS files for marketplace widgets

📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceDataProductsWidget/marketplace-data-products-widget.less:1 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceDomainsWidget/marketplace-domains-widget.less:1
marketplace-data-products-widget.less and marketplace-domains-widget.less are completely identical (35 lines each, same selectors and styles). This means any future style change must be applied in two places, increasing the maintenance burden and risk of divergence. Extract the shared styles into a single shared LESS file (e.g., marketplace-widget-shared.less) and import it from both components.

Security: Missing permission checks on Add buttons in marketplace widgets

📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceDataProductsWidget/MarketplaceDataProductsWidget.component.tsx:187 📄 openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceDomainsWidget/MarketplaceDomainsWidget.component.tsx:187
Both MarketplaceDataProductsWidget and MarketplaceDomainsWidget render "Add Data Product" / "Add Domain" buttons unconditionally without checking user permissions. In contrast, the canonical DomainListPage and DataProductListPage both use usePermissionProvider() to gate their Add buttons behind permissions.domain?.Create and permissions.dataProduct?.Create respectively.

While the server-side API will reject unauthorized requests, the UI should not expose creation workflows to users who lack Create permission. This is inconsistent with the rest of the codebase and will confuse users when the form opens but submission fails.

Fix: Import usePermissionProvider and conditionally render the Add buttons:

const { permissions } = usePermissionProvider();
// ...
{permissions.domain?.Create && (
  <Button onClick={openDrawer}>...</Button>
)}
Bug: moveWidget mutates React state objects in-place

📄 openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDataMarketplacePage/CustomizableDataMarketplacePage.tsx:128 📄 openmetadata-ui/src/main/resources/ui/src/pages/CustomizableDataMarketplacePage/CustomizableDataMarketplacePage.tsx:133
In moveWidget, const newLayout = [...layout] creates a shallow copy of the array, but each element is still a reference to the original state object. The subsequent widget.y = cumulativeY inside forEach mutates the .y property directly on the original state objects before setLayout is called. This is a direct React state mutation that poisons snapshot comparisons — notably the disableSave memo which uses fast-json-patch's compare() against the original page state. Since the original objects are mutated in-place, the diff may incorrectly report no changes, causing the Save button to remain disabled after reordering widgets.

...and 24 more resolved from earlier reviews

🤖 Prompt for agents
Code Review: Adds customizable Data Marketplace landing page with 13 open issues including critical findings: MarketplaceNavBar duplicates 500+ lines from NavBar, missing cleanup of polling setTimeout on unmount, showErrorToast firing on expected 404s, and global domain/data-product path functions coupled to mutable store state. Address these blockers before approval.

1. ⚠️ Quality: MarketplaceNavBar is a ~500-line copy-paste of NavBar
   Files: openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:76, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:159, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:76-90, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:97-111, openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx

   MarketplaceNavBar is almost entirely duplicated from NavBar.tsx (~500 lines), including all websocket handling, notification logic, version-mismatch alerts, and browser notification code. This duplication means every future bug fix or feature addition to NavBar must be mirrored here, which is a significant maintenance burden. It also inherits the existing `handleBellClick` stale-closure bug (dependency array `[hasTaskNotification]` is missing `hasMentionNotification` and `activeTab`).
   
   Consider extracting the shared logic (socket listeners, notifications, version check, language switcher, help menu, user profile) into a shared hook or a common `NavBarBase` component, and composing the two variants from it. MarketplaceNavBar's only unique element is the `<MarketplaceNav />` dropdown replacing the search bar.

   Suggested fix:
   Extract shared navbar logic into a custom hook (e.g. useNavBarState) or a NavBarShell component that accepts a `leftContent` slot. MarketplaceNavBar and NavBar would both compose from that shared base, each providing their own left-side content (search bar vs marketplace nav).

2. 💡 Edge Case: NPE risk in getTags cert-tag filter if tagFQN is null

   In `getTags`, the `removeIf` predicate calls `FullyQualifiedName.getParentFQN(tag.getTagFQN())`. If `tag.getTagFQN()` returns null (e.g., due to corrupt data or incomplete migration), `getParentFQN` will call `CharStreams.fromString(null)` and throw a NullPointerException. While `tagFQN` is required in the schema, the database may contain legacy rows without it. A null guard would make this defensive.

   Suggested fix:
   tags.removeIf(
       tag -> tag.getTagFQN() != null
           && certClassification.equals(FullyQualifiedName.getParentFQN(tag.getTagFQN())));

3. 💡 Performance: getCertificationClassification called on every getTags invocation
   Files: openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts:140-154, openmetadata-ui/src/main/resources/ui/src/hooks/useMarketplaceStore.ts:24-38, openmetadata-ui/src/main/resources/ui/src/pages/DataMarketplacePage/MarketplaceLayout.component.tsx:25-29, openmetadata-ui/src/main/resources/ui/src/hooks/useMarketplaceStore.ts, openmetadata-ui/src/main/resources/ui/src/utils/EntityUtilClassBase.ts:358-364, openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts:169-183, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceDataProductsWidget/MarketplaceDataProductsWidget.component.tsx:150-152, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceSearchBar/MarketplaceSearchBar.component.tsx:128-129, openmetadata-ui/src/main/resources/ui/src/hooks/useMarketplaceStore.ts:26-38, openmetadata-ui/src/main/resources/ui/src/pages/DataMarketplacePage/MarketplaceLayout.component.tsx:25-28

   Every call to `getTags(fqn)` now calls `getCertificationClassification()`, which reads from `SettingsCache`. When listing entities (e.g., paginated list of 100 tables with tags), this results in 100+ cache lookups per request. While `SettingsCache` is in-memory, the repeated deserialization of `AssetCertificationSettings` and string allocation could add up in hot paths. Consider caching the classification name in a field (invalidated on settings change) or computing it once per request.

4. ⚠️ Bug: entityIconMapping drops SearchIndex.ML_MODEL_SERVICE ('mlModelService')

   The refactoring that moved `entityIconMapping` from inside `getEntityIcon()` to module scope removed `SearchIndex.ML_MODEL_SERVICE` (value `'mlModelService'`), assuming it was a duplicate of `EntityType.MLMODEL_SERVICE` (value `'mlmodelService'`). However, these are **different strings** (capital 'M' in 'Model' vs lowercase). Any code path that calls `getEntityIcon('mlModelService')` — e.g., search results displaying ML model services — will now fall through to the default case and show no icon.
   
   Other removed entries (e.g., `SearchIndex.MESSAGING_SERVICE`, `SearchIndex.STORAGE_SERVICE`) happen to have the same string values as their `EntityType` counterparts and are fine.

   Suggested fix:
   Add the missing entry back to the module-level mapping:
   
     [SearchIndex.ML_MODEL_SERVICE]: MlModelIcon,
   
   after the existing `[EntityType.MLMODEL_SERVICE]: MlModelIcon,` line.

5. 💡 Security: AppRunLogAppender path traversal tests missing

   The `resolveLogFile` and `resolveAppDir` methods in `AppRunLogAppender` correctly implement path traversal protection via `normalize() + startsWith(base)`. However, the entity name schema (`^((?!::).)*$`) does not block path components like `../` or `/`, meaning the runtime check is the sole defense. The test suite (`AppRunLogAppenderTest`) doesn't include any path traversal test cases to verify this protection continues working.

   Suggested fix:
   Add a test case to AppRunLogAppenderTest:
   
   @Test
   void startCapture_rejectsPathTraversal() {
     assertThrows(IllegalArgumentException.class,
       () -> AppRunLogAppender.startCapture(
         "123", "id", "../../etc", "server1"));
   }

6. 💡 Security: resolveBaseUrl() falls back to http://localhost:8585

   The `resolveBaseUrl()` method in `McpCallbackServlet` returns `"http://localhost:8585"` as a hardcoded fallback (line 124) when both the MCP config and system settings fail to provide a base URL. This value is used to construct the session redirect URI at line 296 (`baseUrl + "/mcp/callback"`). In production, if configuration loading fails transiently, the OAuth redirect would target localhost instead of the real server, causing the callback to fail silently. While this isn't an open redirect in the traditional sense (the attacker can't control the URL), it could cause confusing auth failures in production and the use of `http://` (not `https://`) for a security-sensitive callback is a concern.

   Suggested fix:
   Consider throwing an IllegalStateException instead of silently falling back:
   
   throw new IllegalStateException(
       "Cannot resolve base URL for MCP callback. "
       + "Configure mcpConfiguration.baseUrl or openMetadataBaseUrl.");

7. ⚠️ Edge Case: showErrorToast fires on expected 404 when persona has no layout
   Files: openmetadata-ui/src/main/resources/ui/src/pages/DataMarketplacePage/DataMarketplacePage.component.tsx:103-105, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/AnnouncementsWidgetV2/AnnouncementsWidgetV2.component.tsx:122-124, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/AnnouncementsWidgetV2/AnnouncementsWidgetV2.component.tsx:68-82, openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/AnnouncementsWidgetV2/AnnouncementsWidgetV2.component.tsx:74-79

   When a persona has no customization document, `getDocumentByFQN` returns a 404. The new `showErrorToast` call in the catch block will display an error notification to the user for this normal, expected condition. Other callers like `MyDataPage` silently catch this error, and `CustomizablePage` explicitly checks for 404 status to handle it gracefully.
   
   Users who haven't customized their marketplace page will see an error toast every time they visit.

   Suggested fix:
   } catch (error) {
     if ((error as AxiosError).response?.status !== ClientErrors.NOT_FOUND) {
       showErrorToast(error as AxiosError);
     }
     setLayout(defaultLayout);
   }

8. 💡 Bug: useEffect missing deps: currentPage, handlePagingChange
   Files: openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx:366-370

   The pagination sync `useEffect` at line 137 uses `handlePagingChange`, `currentPage`, and `handlePageChange` in its body, but the dependency array is `[allTasksInternal.length, pageSize]`. While `handlePagingChange` (which is `setPaging`) is stable, `currentPage` is read directly and `handlePageChange` is recreated when `currentPage` changes. This means the `currentPage > maxPage` correction (line 140) won't re-run when `currentPage` changes externally (e.g. via URL params). The eslint `exhaustive-deps` rule would flag this.

   Suggested fix:
   useEffect(() => {
     handlePagingChange({ total: allTasksInternal.length });
     const maxPage = Math.max(1, Math.ceil(allTasksInternal.length / pageSize));
     if (currentPage > maxPage) {
       handlePageChange(maxPage, { cursorType: null, cursorValue: undefined });
     }
   }, [allTasksInternal.length, pageSize, currentPage, handlePagingChange, handlePageChange]);

9. 💡 Quality: Hardcoded hex colors in 'Load more' badge

   The new "Load more" badge uses hardcoded `'#ffffff'` and `'#155EEF'` (lines 445-447 of useOntologyGraph.ts) while the adjacent asset-count badge correctly uses `NODE_FILL_DEFAULT` and `NODE_LABEL_FILL` constants. This is inconsistent with the PR's own stated goal of replacing hardcoded colors, and makes the badge immune to theming. `'#155EEF'` (the primary blue) has no named constant exported from the constants file.

   Suggested fix:
   // Extract to constants in OntologyExplorer.constants.ts:
   export const DATA_MODE_LOAD_MORE_BG = '#155EEF';
   // Then use:
   fill: NODE_FILL_DEFAULT,
   backgroundFill: DATA_MODE_LOAD_MORE_BG,

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copilot AI 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.

Pull request overview

Copilot reviewed 102 out of 105 changed files in this pull request and generated 1 comment.

Comment on lines +88 to +92
<Link
className="flex-shrink-0 tw:bg-transparent"
id="openmetadata_logo"
to="/">
{collapsed ? (

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

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

This file uses Tailwind classes, but flex-shrink-0 is missing the required tw: prefix (Tailwind is imported with prefix(tw)), so the class won’t apply. Update it to tw:flex-shrink-0 (or equivalent) to ensure the logo link doesn’t shrink in the collapsed state.

Copilot uses AI. Check for mistakes.
@sonarqubecloud

sonarqubecloud Bot commented Apr 6, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Apr 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants