Portal NFS composition¶
For package/runtime Mermaid diagrams, see SDK architecture. For contributor repository structure, APIs, examples and acceptance requirements, see the third-party plugin author guide.
Composition model¶
The host owns the entity.detail.tabs interaction and placement. Contributors
declare static Blueprint extensions in their ./alpha entry points.
NFS supplies typed entity, catalog, and scaffolder-field descriptors to api:portal/contributions,
an app-scoped API extension owned by the portal frontend plugin. Pages share
its validated snapshot through portalContributionsApiRef. There is no native global contribution registry,
module-load registration, or imperative module resolver in this path.
The adapters live in @ansible/portal-extension-api/alpha and
@ansible/portal-extension-host/alpha. Shared extension data references use
stable IDs rather than a shared mutable singleton. Installing a plugin loads
its declarations, but component loaders are deferred until filtering and
authorization complete. Only the selected eligible tab loads its component.
The provider uses ApiBlueprint, avoiding an app-root wrapper that the
installed Backstage API restricts to the app plugin. Scaffolder fields use the
shared provider rather than attaching directly to an optional templates page.
Operations remain separate from presentation and receive the current subject,
not contributor-supplied URLs.
Integrating the host¶
Prerequisites¶
- An NFS app with a host frontend plugin whose plugin ID is
portal. @backstage/frontend-plugin-api0.17.1 and compatible platform packages.- A host page that resolves the current entity and canonical content type.
- A host entitlement resolver for declared entitlement requirements.
Procedure¶
- Install
createPortalContributionProvider()from@ansible/portal-extension-host/alphaonce in theportalfrontend plugin. The provider's extension ID isapi:portal/contributions. - Extend the existing host layout with
useNativeEntityPageandNativeEntityActionMenu; useNativeCatalogItemActionsinside an existing catalog row menu.createPortalEntityPageandcreatePortalCatalogPageremain reusable host factories, but installing Portal does not create a second catalog or entity UI. - Render
PortalEntityTabswith the descriptors and anEntityContributionContext. The host supplies the entity, canonical content type, and optional source observations. Wrap legacy component trees withcompatWrapper, as demonstrated in the composition tests. - Supply
availableExperienceswhen an experience is disabled. - Supply
checkEntitlementfrom host authorization. It receives the declared entitlement and subject; the host resolves trusted organization memberships. Missing resolvers, denied decisions, and failed checks suppress entitled tabs. - Install the contributor's NFS feature through app discovery or dynamic
loading. Do not import its legacy
index.tsfor side effects. - Rebuild the host and contributors together. Earlier extension declarations
attached to
page:portal/entity; they must use the shared provider attachment.
Verification¶
From the repository root, run:
yarn test --runInBand \
workspaces/portal-core/plugins/portal-extension-host \
workspaces/portal-scaffolder/plugins/portal-scaffolder/src/alpha.test.tsx \
workspaces/portal-scaffolder/plugins/portal-scaffolder/src/alphaPermissions.test.tsx \
workspaces/apme/plugins/backstage-apme/src/alpha.test.tsx
The host tests exercise real NFS discovery, filtering, selection, permission denial, configuration and experience gates, entitlement fail-closed behavior, manifest rejection, and failure isolation. The APME test uses its actual NFS declaration and substitutes the Quality component to avoid a live backend. Shared-provider tests also cover cross-page discovery, NFS-disabled extensions, operation subjects, follow-on navigation, and refresh of already-selected results. Field composition tests cover absent and alternative templates hosts, first- and third-party fields, duplicate and malformed declarations, and preservation of field schemas and asynchronous validation. The separate scaffolder permission suite checks catalog-import denial without reusing the route-parity suite's cached permission decisions.
Optional templates hosts¶
Portal host services and AAP authentication do not require portal-scaffolder. Application and deployment composition can omit that contributor. Install independent native plugins through their declared frontend features and normal RHDH dynamic plugin configuration; shared SDK libraries are not standalone dynamic page plugins. Existing Collections, Execution Environments, and Git repositories pages still belong to portal-scaffolder until content extraction. APME's current repository UI therefore still needs that contributor.
ScaffolderFieldBlueprint attaches declarations to api:portal/contributions
through its scaffolderFields input. At app startup, the Portal provider checks
actual NFS ownership, manifest references, API compatibility, content scope,
field names, and duplicate names/entry-point IDs. It publishes accepted
declarations as the read-only portalContributionsApiRef.scaffolderFields
snapshot without invoking widget loaders.
createPortalScaffolderPage is an optional consumer adapter. When its route is
opened, it supplies the shared declarations to the templates implementation.
NativeScaffolderFields checks configuration, experience availability,
permissions, and reserved field names before loading widgets. The returned
field name must match the declaration; value/UI schemas and asynchronous
validators remain part of the existing field contract.
Without a templates adapter, declarations remain discoverable but unused. No
widget is loaded and no missing templates-page attachment is required. An
alternative templates plugin can use the same adapter without taking the
portal-scaffolder plugin ID. Field discovery does not make workflows runnable
without a templates host. Application/deployment selection is separate from
field discovery and does not itself extract the existing content pages.
Root routing is configured separately through the Portal host's landing
destination.
Migrating field attachments¶
Prerequisites: install the shared Portal provider and build the host, templates adapter, and native contributors from a coordinated SDK revision.
- Keep existing
ScaffolderFieldBlueprint.makedescriptors, field names, manifest IDs, widget schemas, and validation functions. Rebuild the shared API and host packages, then rebuild/re-export the consuming native plugins together. - Remove explicit NFS attachment overrides targeting
page:portal-scaffolder/templates. The Blueprint now defaults toapi:portal/contributions, inputscaffolderFields. Do not restore a second page collector or legacy registration side effects. - Keep
createPortalScaffolderPagefor the templates route. Custom composition tests must installcreatePortalContributionProvider()in theportalplugin, just as the real app does. The page helper retains its loader/route options; an explicithostApiVersionadds an adapter-specific restriction to the provider's version validation.
Verify that a permitted field renders with the templates host installed and that removing that host leaves the declaration inspectable without calling its widget loader. The field tests in the repository verification command exercise both cases. Coordinated rebuilds are required; this migration does not promise mixed old/new dynamic artifact compatibility or require npm publication.
Contribution contract¶
EntityDetailTabBlueprint.make accepts a name and a descriptor:
manifest: plugin metadata and capability declarations.capabilityId,entryPointId: references to the capability and entity-tab entry point. Labels, order, content scope, and permission come from the entry.attachment: exactlyentity.detail.tabsfor this contract.loader: deferred component loader receiving the current subject.appliesToSourceKinds: optional source constraints.enabledByConfig: optional configuration key; explicittrueis required.requiredEntitlements: optional IDs declared in the manifest.isApplicable: optional synchronous or asynchronous predicate. It narrows scope; it cannot broaden it. Exceptions suppress the tab.
The entry must have kind entity-tab, surface entity-page, and a slot launch
targeting entity.detail.tabs. Its content scope must be non-empty, known, and
within capability scope. Runtime predicates stay on the descriptor, not in
serializable capability metadata.
The host checks ownership against the actual NFS plugin, API compatibility, references, region, content scope, and duplicate IDs. Invalid descriptors are logged and omitted without removing the rest of the page. Filtering proceeds through content/source scope, experience/configuration, permission, entitlement, and predicate before component loading. Selected components are error-bounded.
Supported native launches¶
The internal developer-preview contract defines the author-facing boundary and proposed compatibility policy. Native launch support is limited to the following combinations:
- Slots render through the matching declared Blueprint and its allowed host
region. The host does not resolve arbitrary
moduleNamevalues. - Entity and catalog item actions dispatch registered operations for the current subject.
- Entity operation actions may select an eligible detail tab after a successful
response. The manifest follow-on must target
entity.detail.tabs, and the descriptor must identify the matching tab withfollowOnEntryPointId. The destination must also pass eligibility checks. Selection does not wait for a background job to complete. - Catalog-header workflow actions resolve owner-prefixed workflow IDs through
ScaffolderWorkflowBlueprintto template entity references, not supplied URLs.
Catalog item action follow-ons, operation-to-workflow follow-ons, arbitrary
launch chains, and workflow launches on other action surfaces are unsupported.
The common CapabilityLaunch union is broader than the native renderer subset;
legacy launcher behavior is not the native contract.
APME entry point¶
@ansible/plugin-backstage-apme/alpha default-exports an apme frontend plugin.
It declares the Quality tab, overview summary, scan action, catalog action,
and repository URL field, and registers the existing
API factory through ApiBlueprint. It does not import plugin.ts or register
legacy contributions.
The native declarations also cover repository Quality activity and Dependencies,
catalog Quality/settings/Violations tabs, table columns, the Add repository
workflow launcher, and deregistration. Scanning requires
ansible.apme.enabled: true and the host-resolved
apme.scanning entitlement. Configuration alone is not an entitlement decision.
The complete repository UI journey still uses portal-scaffolder's Git Repositories list/detail pages and its Templates UI for Add repository. APME has no declared package dependency on portal-scaffolder, but these presentation and navigation paths still require their current host. Its Content Quality route redirects into the repository catalog. Collecting fields independently does not extract those pages or make that full UI journey available without them.
Acceptance boundaries¶
- The packed
next-2.1deployment uses mock AAP authentication. The source NFS app was verified with real AAP login and two completed standalone checks on the existingmy-ns-my-coltest repository. Persisted scan count increased from two to four, with one finding. APME refreshes its summary while a background operation is active, then stops polling; the Portal host does not need scan-specific logic. No remediation, branch, or PR was applied. - The named
test-amazon-awsmanual entry was re-registered through the native field/workflow and deregistered through its native entity action. Catalog count changed from 123 to 124 to 123; confirmed removal redirected without a manual page reload. The existing APME project ID and zero-scan history were unchanged. These counts are test evidence, not expected deployment defaults. - The earlier AWS dependency-resolution failure is not evidence against native discovery, but scans of that larger repository still need their own successful assessment evidence. Do not weaken Galaxy server validation to make them pass.
- Complete content extraction, query/write contracts, and picker migration.
- Compatibility validation is intentionally limited to
next-2.1for this slice. The AAP resolver validates caller memberships against configured organizations. REST operations, registration adapters, and APME interactive workflows use the same resolver, independently of identity namespace. Any granted membership enables a shared capability, subject to operation and repository RBAC. Multiple memberships do not select an active organization for organization-specific handlers. Git repositories need no AAP ownership annotation or namespace mapping; new manual entries stay indefault. Remaining legacy mutations and automatic scan policy are not guarantees supplied by frontend gates. See the operations backend policy. - The native app does not import APME's legacy entry point for registration side effects. The explicitly selected legacy fallback still has its own registration. Do not install both registration paths in the same native app.
Navigation and local app shell¶
Loading AAP authentication in a dynamic host¶
The @ansible/aap-app-auth frontend module owns the AAP OAuth API and
sign-in page. A dependency in portal-scaffolder/package.json does not install
that module into an RHDH host. Each NFS deployment using AAP authentication
must explicitly load its exported dynamic artifact alongside the Portal plugins.
With the exported tarball available in the host's local plugin directory, add
this entry to the deployment's dynamic-plugins.yaml plugins list:
For a workspace-mounted development export, use the directory instead:
These are alternative artifact paths, not two registrations. Adjust the path
to match the deployment's exported artifact. NFS discovers the module's
./alpha entry point from its package metadata; no Scalprum signInPage or
apiFactories configuration is required.
After exporting and restarting the host, verify that api:app/rhaap-auth and
sign-in-page:app/rhaap are discovered. An absent AAP sign-in page or an
unresolved ansible.auth.rhaap API is a reason to check the installed dynamic
plugin list, not to restore authentication exports in portal-scaffolder.
Landing behavior¶
The Portal host owns /. With no portal.landingPage override, it opens
/self-service only if that NFS page is mounted. If no destination is available,
Portal displays a neutral welcome message inside the existing app shell instead
of redirecting to a missing page. This fallback is not a separate content catalog
or a new homepage design.
An explicit override must match an installed top-level NFS page's effective
path, including any NFS page-path configuration override. The host checks
mounted metadata without invoking page loaders. It rejects external URLs,
relative paths, /, dot segments, encoded paths, query strings, fragments,
trailing slashes, and arbitrary descendants of a registered page. A missing or
disabled destination also uses the fallback. An invalid explicit override does
not silently select Templates or the first sidebar item.
Landing selection is not authorization: navigation enters the page through its normal NFS lifecycle, and the destination retains its permission checks. A restricted destination may show its normal access-denied state. Sidebar ordering and visibility do not grant access or change landing selection.
Configuring the landing destination¶
Prerequisites: install the Portal native frontend host and the destination page
in the NFS app. No additional landing configuration is needed for the default
Templates behavior; omit portal.landingPage in the minimal configuration.
- To override the default, set the exact installed page path in host config:
This is the complete landing configuration; the field is optional. Paths are app-relative: if the app is hosted under a URL prefix, do not include that prefix or a host name in this value.
- Restart the source app after changing configuration. For RHDH, export/install the updated Portal artifact and restart the host with the configuration.
- Open the app root. Verify that it opens the selected page and that the page's normal permission checks still apply. With an unavailable destination, verify the neutral fallback rather than a missing-page redirect.
Migration: existing deployments with Templates at /self-service need no new
setting. If NFS configuration moves that page, set portal.landingPage to its
effective path. Deployments without portal-scaffolder may select another
installed page or leave the host fallback. This does not remove portal-scaffolder
from source-app or installer composition; deployment selection is separate.
Verify the routing behavior with:
The tests cover configured and default destinations, absent and disabled contributors, page-path overrides, unsafe destinations, lazy-loader isolation, and destination permission denial.
Configuring navigation¶
Plugins own page routes and icon metadata. Portal navigation selects discovered pages; it does not install plugins or grant permissions. The self-service native pages provide the same Home, History, Build, Collections, and GitHub icons as the legacy sidebar. RBAC and Settings retain their platform plugin icons.
Enable the Portal navigation adapter by disabling the stock singleton attachment:
app:
extensions:
- nav-content:app/sidebar: false
- nav-content:app/portal: true
portal:
navigation:
items:
- id: templates
path: /self-service
title: Templates
An item selects exactly one path or NFS page extension ID. Optional title
overrides its label, and placement: bottom pins it below the primary items.
An expandable group instead declares a title and child links:
portal:
navigation:
topOffset: 64
items:
- id: administration
title: Administration
placement: bottom
children:
- id: rbac
page: page:rbac
title: RBAC
- id: settings
page: page:user-settings
title: Settings
placement: bottom
To make Administration navigate directly to RBAC, replace children with
page: page:rbac. This works for any item, not only Administration. Missing
plugin pages and empty groups are omitted; IDs and route targets must be unique.
Group icons inherit the first available child icon; icon overrides are not a
navigation config field. The paths/bottomPaths configuration remains a
compatibility fallback only when items is absent. Nested item data is exposed
through the host's @deepVisibility frontend schema.
The source app registers native RBAC, user settings, and Search features. Its global header reuses the existing AAP/identity logout behavior through an app-owned root wrapper. Create is omitted when the installed Templates adapter is unavailable; it follows that adapter's effective route when present. RHDH retains its own platform header instead of loading the source app wrapper. Guest users can see navigation but remain subject to page permissions; an RBAC 403 is not a missing route.
Restart the source dev server after changing configuration or schema visibility. Re-export and restart RHDH after changing dynamic plugins. The local development guide in automation-portal-local documents workspace/packed exports, the full navigation example, cache validators, and non-destructive refresh steps.
Task history and RBAC¶
The native frontend provides Task history at /self-service/create/tasks, with
task details below that path. It has a separate navigation entry alongside the
template wizard. The stock RBAC frontend is a platform plugin, not a Portal
contribution; permission configuration alone does not install its UI. Native
deployment must explicitly enable its package without broadening administrator
identities or permission grants.
Troubleshooting¶
Installed contributor has no tab¶
Check api:portal/contributions, installed NFS features, current content type, configuration, experience,
permission, and entitlement decisions. Inspect [Portal NFS] Rejected entity tab
and [Portal NFS] Cannot activate diagnostics. Unsupported content is omitted,
not shown as an error.
Task history or RBAC is missing¶
Open /self-service/create/tasks directly to distinguish missing navigation
from a route or permission failure. Check page:portal-scaffolder/history and
the deployed bundle. For RBAC, inspect installed dynamic packages and the
existing administrator configuration; do not bypass restrictions with broader grants.
Template field is declared but unavailable¶
Inspect the NFS extension tree for the field under api:portal/contributions,
input scaffolderFields, and inspect the contribution API's validated field
list in a host integration or test. A valid declaration with no templates
consumer is intentionally unused; no widget import is expected.
If the field is missing from the snapshot, check native discovery, ownership, manifest/version compatibility, and duplicate IDs or field names. If it is present but an installed templates adapter does not expose it, check the configuration, experience, permission, and reserved-name gates. Loader or widget failures use the existing template-field error states. There is no separate field diagnostics page or automatic fallback registration.
Component fails to load¶
The selected tab displays a contained failure while the host page remains available. Check the contribution ID, failed chunk request, remote assets, and compatible platform versions before changing the contribution contract.