Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/nfs-shell-and-scaffolder-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@openchoreo/backstage-plugin': minor
'@openchoreo/backstage-plugin-openchoreo-observability': minor
'@openchoreo/backstage-plugin-platform-engineer-core': minor
'@openchoreo/backstage-plugin-react': minor
'@openchoreo/backstage-portal-app': minor
---

Complete the New Frontend System migration for the portal shell and
distribute scaffolder field extensions through the base plugin.

**Portal**: `convertLegacyAppRoot` and `Root.tsx` are gone. Themes, icons,
sidebar (`NavContentBlueprint`), provider stack, and every route now
ship as NFS blueprints.

**Adopter-facing additions**:

- `@openchoreo/backstage-plugin/alpha` — `execTerminalPage`, 32
`FormFieldBlueprint`s for OC template fields, plus new component
exports (`ScaffolderPreselectionProvider`, `EntityWarningStrip`,
`ForeignCardsSection`)
- `@openchoreo/backstage-plugin-openchoreo-observability/alpha` —
`costInsightsPage` with sidebar auto-discovery
- `@openchoreo/backstage-plugin-platform-engineer-core/alpha` —
`platformOverviewPage` with sidebar auto-discovery; `PlatformOverviewPage`
source moved from portal-app
- `@openchoreo/backstage-plugin-react` — `useQueryParams` (backwards-compat
re-export left in `@openchoreo/backstage-plugin`)
11 changes: 10 additions & 1 deletion app-config.production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ app:
- runtime: { title: Runtime, contentOrder: natural }
- analysis: { title: Analysis, contentOrder: natural }
- external: { title: External }
# Suppress upstream cards that duplicate OC layouts.
# Suppress upstream cards that duplicate OC layouts + default themes so
# our openchoreo-* themes win the theme picker.
extensions:
- theme:app/light: false
- theme:app/dark: false
# `page:kubernetes` needs an entity context (throws standalone); we
# want only the entity-page tab from the kubernetes plugin.
- page:kubernetes: false
- entity-card:catalog/about: false
- entity-card:catalog/links: false
- entity-card:catalog/labels: false
Expand All @@ -36,6 +42,9 @@ app:
- entity-card:api-docs/provided-apis: false
- entity-card:api-docs/consumed-apis: false
- entity-card:api-docs/has-apis: false
# ApiDefinitionCard lives on the Definition tab; hide the standalone.
- entity-card:api-docs/definition: false
# Providers/Consumers rendered directly by ApiOverviewLayout.
- entity-card:api-docs/providing-components: false
- entity-card:api-docs/consuming-components: false
# GitLab cards throw when GitLab annotations are absent.
Expand Down
16 changes: 15 additions & 1 deletion app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,16 @@ app:
- runtime: { title: Runtime, contentOrder: natural }
- analysis: { title: Analysis, contentOrder: natural }
- external: { title: External }
# Suppress upstream cards that duplicate OC layouts.
# Suppress upstream cards that duplicate OC layouts + default themes so
# our openchoreo-* themes win the theme picker.
extensions:
- theme:app/light: false
- theme:app/dark: false
# `page:kubernetes` is the plugin's standalone /kubernetes page — it
# calls useEntity() inside its router and throws when opened without
# an entity context. We only want the plugin's entity-page tab, not
# this broken top-level page.
- page:kubernetes: false
- entity-card:catalog/about: false
- entity-card:catalog/links: false
- entity-card:catalog/labels: false
Expand All @@ -47,6 +55,12 @@ app:
- entity-card:api-docs/provided-apis: false
- entity-card:api-docs/consumed-apis: false
- entity-card:api-docs/has-apis: false
# ApiDefinitionCard already lives on the Definition tab; hide the
# standalone card on API Overview to avoid duplication.
- entity-card:api-docs/definition: false
# Providers/Consumers rendered directly by ApiOverviewLayout, so
# suppress the auto-contributed cards to prevent duplicates in
# ForeignCardsSection.
- entity-card:api-docs/providing-components: false
- entity-card:api-docs/consuming-components: false
# GitLab cards throw when GitLab annotations are absent.
Expand Down
33 changes: 9 additions & 24 deletions packages/portal-app/src/apis.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
/**
* Smoke test for the API factories registered in `apis.ts`.
*
* The render-the-whole-app test in App.test.tsx exercises most factories
* indirectly, but the perch-agent factory is wired through a route
* the smoke render never visits, so its body shows up as 0% covered on
* the codecov diff. This file calls each factory directly with the
* minimum dependency surface so the body is executed.
*
* We only assert "factory returned an instance" — full client behavior
* is covered by each plugin's own tests.
*
* Under NFS, `openchoreo-ci`, `openchoreo-workflows`, and the
* `catalog-graph` override own their API factories via `ApiBlueprint`
* inside their `/alpha` plugins / `customOverrides.tsx`, so they are
* NOT in this app-scoped `apis` array. Their factory bodies are covered
* by the plugins' own tests.
* OpenChoreo-specific fetch / permission / auth factories now ship from
* `@openchoreo/backstage-plugin` as ApiBlueprints, so they're not in this
* app-scoped `apis` array — covered by the plugin's own tests.
*/
import {
AnyApiFactory,
fetchApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { visitsApiRef } from '@backstage/plugin-home';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import {
Expand All @@ -30,10 +18,7 @@ import {
} from '@openchoreo/backstage-plugin-openchoreo-portal-assistant';

import { apis } from './apis';
import { openChoreoAuthApiRef } from './apis/authRefs';

// Minimal stubs — none of the factories under test inspect dep state at
// construction time beyond holding the reference.
const stubDiscovery = { getBaseUrl: async () => 'http://localhost' } as any;
const stubFetch = { fetch: globalThis.fetch ?? (() => undefined) } as any;
const stubIdentity = {
Expand All @@ -54,9 +39,6 @@ function findFactory(
}

function invoke(factory: AnyApiFactory, deps: Record<string, unknown>) {
// ``factory`` is an internal type — call its ``factory`` member with
// the requested deps. The deps object is whatever ``factory.deps``
// declares; we provide stubs keyed by the same names.
return (factory as any).factory(deps);
}

Expand All @@ -65,9 +47,6 @@ describe('apis registry', () => {
const ids = apis.map(f => f.api.id);
for (const ref of [
scmIntegrationsApiRef,
permissionApiRef,
fetchApiRef,
openChoreoAuthApiRef,
visitsApiRef,
storageApiRef,
perchAgentApiRef,
Expand All @@ -76,6 +55,12 @@ describe('apis registry', () => {
}
});

it('does NOT register OC-specific fetch/permission/auth factories (base plugin owns them)', () => {
const ids = apis.map(f => f.api.id);
expect(ids).not.toContain(fetchApiRef.id);
// permission and openchoreo-auth are also owned by the base plugin now
});

it('builds the PerchAgentClient via its factory', () => {
const f = findFactory(apis, perchAgentApiRef);
const instance = invoke(f, {
Expand Down
99 changes: 8 additions & 91 deletions packages/portal-app/src/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,111 +8,33 @@ import {
configApiRef,
createApiFactory,
discoveryApiRef,
oauthRequestApiRef,
errorApiRef,
identityApiRef,
fetchApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { OAuth2 } from '@backstage/core-app-api';
import { VisitsWebStorageApi, visitsApiRef } from '@backstage/plugin-home';

// No explicit default theme id is seeded into localStorage. Backstage's
// user-settings panel renders a built-in Auto button that picks the first
// registered dark or light theme based on the OS `prefers-color-scheme`
// preference, which is exactly the behavior we want for new users.
import { UserSettingsStorage } from '@backstage/plugin-user-settings';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { OpenChoreoFetchApi } from './apis/OpenChoreoFetchApi';
import { OpenChoreoPermissionApi } from './apis/OpenChoreoPermissionApi';
import { openChoreoAuthApiRef } from './apis/authRefs';
// NOTE: ``perchAgentApiRef`` is also declared on
// ``openchoreoPerchPlugin.apis`` in plugins/openchoreo-portal-assistant/src/plugin.ts.
// That declaration is NOT picked up by the app at runtime because the plugin
// exports plain React components — it never registers a routable or
// component extension, so Backstage's plugin loader never visits its
// ``apis`` array. The app-level factory below is the one actually wired in;
// removing it causes ``NotImplementedError: No implementation available for
// apiRef{plugin.openchoreo-portal-assistant.service}`` in AssistantDrawerProvider.
// NOTE: `perchAgentApiRef` is also declared on `openchoreoPerchPlugin.apis`
// in plugins/openchoreo-portal-assistant/src/plugin.ts. That declaration is
// NOT picked up at runtime because the plugin only exports plain React
// components. Removing this app-level factory causes NotImplementedError
// in AssistantDrawerProvider.
import {
perchAgentApiRef,
PerchAgentClient,
} from '@openchoreo/backstage-plugin-openchoreo-portal-assistant';

// OpenChoreo fetch/permission/auth factories are contributed by
// `@openchoreo/backstage-plugin` (base) as ApiBlueprints — no manual
// registration needed here.
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),

// Custom PermissionApi that injects IDP token for OpenChoreo authorization
// This is needed because Backstage's default PermissionClient doesn't allow
// custom headers to be injected (it uses cross-fetch directly)
createApiFactory({
api: permissionApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
oauthApi: openChoreoAuthApiRef,
},
factory: ({ configApi, discoveryApi, identityApi, oauthApi }) =>
new OpenChoreoPermissionApi({
config: configApi,
discovery: discoveryApi,
identity: identityApi,
oauthApi,
}),
}),

// Custom FetchApi that automatically injects auth tokens
// This wraps all fetch calls to include Backstage token + IDP token
// When openchoreo.features.auth.enabled is false, IDP token injection is skipped
createApiFactory({
api: fetchApiRef,
deps: {
identityApi: identityApiRef,
oauthApi: openChoreoAuthApiRef,
configApi: configApiRef,
},
factory: ({ identityApi, oauthApi, configApi }) =>
new OpenChoreoFetchApi(identityApi, oauthApi, configApi),
}),

// OpenChoreo Auth provider - works with any OIDC-compliant IDP
createApiFactory({
api: openChoreoAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
const env =
configApi.getOptionalString('auth.environment') ?? 'development';
const scopeStr = configApi.getOptionalString(
'openchoreo.features.auth.scope',
);
const scopes = scopeStr?.split(/\s+/).filter(Boolean) ?? [];
const defaultScopes = scopes.length
? scopes
: ['openid', 'profile', 'email'];
return OAuth2.create({
discoveryApi,
oauthRequestApi,
configApi,
provider: {
id: 'openchoreo-auth',
title: 'OpenChoreo',
icon: () => null,
},
environment: env,
defaultScopes,
});
},
}),
createApiFactory({
api: visitsApiRef,
deps: {
Expand All @@ -122,7 +44,6 @@ export const apis: AnyApiFactory[] = [
factory: ({ identityApi, errorApi }) =>
VisitsWebStorageApi.create({ identityApi, errorApi }),
}),
// User settings storage - enables centralized storage for starred entities and preferences
createApiFactory({
api: storageApiRef,
deps: {
Expand All @@ -133,10 +54,6 @@ export const apis: AnyApiFactory[] = [
},
factory: deps => UserSettingsStorage.create(deps),
}),

// Assistant Agent client (Perch). Mirrors the registration on
// openchoreoPerchPlugin.apis — see the import-site comment for why
// both exist.
createApiFactory({
api: perchAgentApiRef,
deps: {
Expand Down
Loading
Loading