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}
+
);
}