From 8089404c8eaf18006ff3fa9378bfd343b535e6f8 Mon Sep 17 00:00:00 2001 From: leandro-codee Date: Wed, 19 Aug 2026 17:59:08 -0400 Subject: [PATCH] fix: mount contributed entity content tabs --- .../portal-app/src/apis/customOverrides.tsx | 98 +++++++++++++++---- .../catalog/EntityLayoutWithDelete.tsx | 2 + .../catalog/OpenChoreoCatalogEntityPage.tsx | 14 ++- 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/packages/portal-app/src/apis/customOverrides.tsx b/packages/portal-app/src/apis/customOverrides.tsx index 668708d4c..742607c52 100644 --- a/packages/portal-app/src/apis/customOverrides.tsx +++ b/packages/portal-app/src/apis/customOverrides.tsx @@ -10,7 +10,10 @@ import catalogGraphPluginAlphaBase from '@backstage/plugin-catalog-graph/alpha'; import catalogPluginAlphaBase from '@backstage/plugin-catalog/alpha'; import scaffolderPluginAlphaBase from '@backstage/plugin-scaffolder/alpha'; -import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { + coreExtensionData, + createFrontendModule, +} from '@backstage/frontend-plugin-api'; import { createTranslationMessages } from '@backstage/frontend-plugin-api'; import { SignInPageBlueprint, @@ -30,7 +33,11 @@ import { catalogApiRef, entityPresentationApiRef, } from '@backstage/plugin-catalog-react'; -import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { + DefaultEntityPresentationApi, + EntityLayout, +} from '@backstage/plugin-catalog'; import { discoveryApiRef, fetchApiRef, @@ -148,6 +155,33 @@ export function createCachingCatalogApi(deps: { return new CachingCatalogApi(base, getUserRef); } +const LEGACY_ENTITY_CONTENT_PATHS = new Set([ + '/', + '/alerts', + '/api', + '/cell-diagram', + '/definition', + '/deploy', + '/diagram', + '/docs', + '/environments', + '/github-actions', + '/gitlab', + '/incidents', + '/jenkins', + '/kubernetes', + '/logs', + '/metrics', + '/rca-reports', + '/runs', + '/runtime-events', + '/runtime-logs', + '/traces', + '/try-out', + '/wirelogs', + '/workflows', +]); + /** * Override `catalog`'s default `api:catalog/entity-presentation` to provide * kind icons for OpenChoreo-specific entity kinds (Environment, DataPlane, @@ -170,11 +204,9 @@ export function createCachingCatalogApi(deps: { * same data key, so the legacy JSX slots in unchanged. * * NFS-contributed `EntityContentBlueprint`s (in `inputs.contents`) are - * NOT mounted here because every tab the portal needs is already declared - * by `entityPage`. If a future third-party plugin contributes a tab via - * `EntityContentBlueprint`, switch this loader to a - * `factory(originalFactory, { inputs })` form and merge `inputs.contents` - * deduped by path. + * merged into the OpenChoreo entity layouts below. Legacy routes still win + * on path collisions, so a host-authored tab and a plugin-authored tab do + * not render twice. */ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ extensions: [ @@ -226,19 +258,45 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ }), }), catalogPluginAlphaBase.getExtension('page:catalog/entity').override({ - params: { - loader: async () => { - const [{ OpenChoreoCatalogEntityPage }, { entityPage }] = - await Promise.all([ - import('../components/catalog/OpenChoreoCatalogEntityPage'), - import('../components/catalog/EntityPage'), - ]); - return ( - - {entityPage} - - ); - }, + factory(originalFactory, { inputs }) { + return originalFactory({ + params: { + loader: async () => { + const [{ OpenChoreoCatalogEntityPage }, { entityPage }] = + await Promise.all([ + import('../components/catalog/OpenChoreoCatalogEntityPage'), + import('../components/catalog/EntityPage'), + ]); + const seenPaths = new Set(LEGACY_ENTITY_CONTENT_PATHS); + const additionalContent = inputs.contents.flatMap(output => { + const path = output.get(coreExtensionData.routePath); + if (seenPaths.has(path)) { + return []; + } + seenPaths.add(path); + return ( + + {output.get(coreExtensionData.reactElement)} + + ); + }); + return ( + + {entityPage} + + ); + }, + }, + }); }, }), ], diff --git a/packages/portal-app/src/components/catalog/EntityLayoutWithDelete.tsx b/packages/portal-app/src/components/catalog/EntityLayoutWithDelete.tsx index dda88bc0b..7c7e05eaa 100644 --- a/packages/portal-app/src/components/catalog/EntityLayoutWithDelete.tsx +++ b/packages/portal-app/src/components/catalog/EntityLayoutWithDelete.tsx @@ -16,6 +16,7 @@ import { OpenChoreoEntityLayout, useResourceDefinitionPermission, } from '@openchoreo/backstage-plugin-react'; +import { AdditionalEntityContentRoutes } from './OpenChoreoCatalogEntityPage'; const KIND_DISPLAY_NAMES: Record = { system: 'Project', @@ -194,6 +195,7 @@ function EntityLayoutWithDeleteContent({ kindDisplayNames={mergedKindDisplayNames} > {children} + diff --git a/packages/portal-app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx b/packages/portal-app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx index 2724ec90f..d2210aaa5 100644 --- a/packages/portal-app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx +++ b/packages/portal-app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { useEffect, useRef } from 'react'; +import { createContext, useContext, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import useAsyncRetry from 'react-use/esm/useAsyncRetry'; import type { Entity } from '@backstage/catalog-model'; @@ -77,6 +77,13 @@ interface OpenChoreoCatalogEntityPageProps { * fail `OpenChoreoEntityLayout`'s strict Route-child check. */ children: ReactNode; + additionalContent?: ReactNode; +} + +const AdditionalEntityContentContext = createContext(null); + +export function AdditionalEntityContentRoutes() { + return <>{useContext(AdditionalEntityContentContext)}; } /** @@ -89,10 +96,13 @@ interface OpenChoreoCatalogEntityPageProps { */ export function OpenChoreoCatalogEntityPage({ children, + additionalContent, }: OpenChoreoCatalogEntityPageProps) { return ( - {children} + + {children} + ); }