Loading...
,
}));
-// Mock permission hooks from @openchoreo/backstage-plugin-react
+// Mock permission + assistant hooks from @openchoreo/backstage-plugin-react
const mockUseEnvironmentReadPermission = jest.fn();
const mockUseReleaseBindingPermission = jest.fn();
+const mockUsePortalAssistant = jest.fn();
jest.mock('@openchoreo/backstage-plugin-react', () => ({
useEnvironmentReadPermission: () => mockUseEnvironmentReadPermission(),
useReleaseBindingPermission: () => mockUseReleaseBindingPermission(),
+ usePortalAssistant: () => mockUsePortalAssistant(),
ForbiddenState: (props: any) => (
{props.message}
@@ -69,12 +71,29 @@ jest.mock('@openchoreo/backstage-plugin-react', () => ({
),
}));
-// Mock the EnvironmentsRouter (renders child views)
-jest.mock('./EnvironmentsRouter', () => ({
- EnvironmentsRouter: () => (
-
Environments Content
- ),
-}));
+// Mock the EnvironmentsRouter (renders child views). The probe also renders
+// whatever investigate action reached the context, so tests can assert the
+// prop/API precedence without the real detail panel.
+jest.mock('./EnvironmentsRouter', () => {
+ const { useEnvironmentsContext } = jest.requireActual(
+ './EnvironmentsContext',
+ );
+ return {
+ EnvironmentsRouter: () => {
+ const { renderInvestigateAction } = useEnvironmentsContext();
+ return (
+
+ Environments Content
+ {renderInvestigateAction?.({
+ component: 'checkout',
+ caseType: 'runtime_debug',
+ status: 'Failed',
+ }) ?? null}
+
+ );
+ },
+ };
+});
// Mock NotificationBanner
jest.mock('./components', () => ({
@@ -108,6 +127,7 @@ describe('Environments', () => {
canViewBindings: true,
loading: false,
});
+ mockUsePortalAssistant.mockReturnValue({});
});
it('mounts the router during initial load instead of a generic spinner', () => {
@@ -152,6 +172,66 @@ describe('Environments', () => {
expect(screen.getByText('Environments Content')).toBeInTheDocument();
});
+ it('falls back to the assistant integration API for the investigate action', async () => {
+ mockUsePortalAssistant.mockReturnValue({
+ renderInvestigateAction: () =>
,
+ });
+ mockUseEnvironmentData.mockReturnValue({
+ environments: [],
+ loading: false,
+ isRefetching: false,
+ isForbidden: false,
+ refetch: mockRefetch,
+ });
+
+ renderWithRouter(
);
+
+ await waitFor(() => {
+ expect(screen.getByText('from-api')).toBeInTheDocument();
+ });
+ });
+
+ it('prefers an explicit investigate-action prop over the API slot', async () => {
+ mockUsePortalAssistant.mockReturnValue({
+ renderInvestigateAction: () =>
,
+ });
+ mockUseEnvironmentData.mockReturnValue({
+ environments: [],
+ loading: false,
+ isRefetching: false,
+ isForbidden: false,
+ refetch: mockRefetch,
+ });
+
+ renderWithRouter(
+
}
+ />,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('from-prop')).toBeInTheDocument();
+ });
+ expect(screen.queryByText('from-api')).not.toBeInTheDocument();
+ });
+
+ it('provides no investigate action when no assistant is registered', async () => {
+ mockUseEnvironmentData.mockReturnValue({
+ environments: [],
+ loading: false,
+ isRefetching: false,
+ isForbidden: false,
+ refetch: mockRefetch,
+ });
+
+ renderWithRouter();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('environments-router')).toBeInTheDocument();
+ });
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
it('shows forbidden state when API returns forbidden', () => {
mockUseEnvironmentData.mockReturnValue({
environments: [],
diff --git a/plugins/openchoreo/src/components/Environments/Environments.tsx b/plugins/openchoreo/src/components/Environments/Environments.tsx
index b2ea2d258..17ee89cb7 100644
--- a/plugins/openchoreo/src/components/Environments/Environments.tsx
+++ b/plugins/openchoreo/src/components/Environments/Environments.tsx
@@ -23,13 +23,15 @@ import {
ForbiddenState,
useReleaseBindingPermission,
useEnvironmentReadPermission,
+ usePortalAssistant,
} from '@openchoreo/backstage-plugin-react';
export interface EnvironmentsProps {
/**
- * Host-app slot for the deploy-panel "investigate" button. Injected by
- * ``packages/app`` (which owns the portal-assistant dependency) and
- * forwarded to the detail panel via context. See
+ * Host-app slot for the deploy-panel "investigate" button, forwarded to
+ * the detail panel via context. When omitted (the NFS deploy tab mounts
+ * this component propless) it falls back to the assistant integration
+ * API's ``renderInvestigateAction`` slot. See
* ``EnvironmentsContextValue.renderInvestigateAction``.
*/
renderInvestigateAction?: RenderInvestigateAction;
@@ -142,6 +144,13 @@ export const Environments = ({
[notification, refetch, navigateToList],
);
+ // Deploy-panel investigate action: an explicit prop wins; otherwise fall
+ // back to the assistant integration API (absent when no assistant is
+ // installed, so the detail panel renders no affordance).
+ const { renderInvestigateAction: apiInvestigateAction } =
+ usePortalAssistant();
+ const investigateAction = renderInvestigateAction ?? apiInvestigateAction;
+
// Context value
const contextValue = useMemo(
() => ({
@@ -168,7 +177,7 @@ export const Environments = ({
beginAwaitingNewRelease,
selection,
setSelection,
- renderInvestigateAction,
+ renderInvestigateAction: investigateAction,
}),
[
environments,
@@ -193,7 +202,7 @@ export const Environments = ({
awaitingNewRelease,
beginAwaitingNewRelease,
selection,
- renderInvestigateAction,
+ investigateAction,
],
);
diff --git a/plugins/openchoreo/src/components/Environments/EnvironmentsContext.tsx b/plugins/openchoreo/src/components/Environments/EnvironmentsContext.tsx
index 23c097167..4b2b4e939 100644
--- a/plugins/openchoreo/src/components/Environments/EnvironmentsContext.tsx
+++ b/plugins/openchoreo/src/components/Environments/EnvironmentsContext.tsx
@@ -15,33 +15,17 @@ export type Selection =
| null;
/**
- * Scope handed to the host-app-supplied "investigate" slot when a
- * deployment is in a problem state. Mirrors the render-prop pattern used
- * for runtime-log debugging (``RenderLogRowAction`` in the observability
- * plugin): this plugin owns the shape and decides *where* the affordance
- * renders, while ``packages/app`` injects the actual assistant button —
- * so the openchoreo plugin keeps no dependency on portal-assistant.
+ * The investigate-slot shape now lives in the shared assistant-integration
+ * contract (`@openchoreo/backstage-plugin-react`) so the host app and this
+ * plugin agree on it without either depending on an assistant
+ * implementation; re-exported here for existing consumers.
*/
-export interface InvestigateScope {
- /** Control-plane namespace of the component. */
- namespace?: string;
- /** Project the component belongs to. */
- project?: string;
- /** The component whose deployment is in trouble. */
- component: string;
- /** Environment resource name the binding is in. */
- environment?: string;
- /**
- * Which assistant flow to launch: ``dependency_pending`` when the cause
- * is an unresolved connection, otherwise ``runtime_debug``.
- */
- caseType: 'dependency_pending' | 'runtime_debug';
- /** Human-readable deployment status, e.g. ``Pending`` / ``Failed``. */
- status: string;
-}
+import type {
+ InvestigateScope,
+ RenderInvestigateAction,
+} from '@openchoreo/backstage-plugin-react';
-/** Render-prop slot for the deploy-panel investigate affordance. */
-export type RenderInvestigateAction = (scope: InvestigateScope) => ReactNode;
+export type { InvestigateScope, RenderInvestigateAction };
interface EnvironmentsContextValue {
/** All environments loaded from the API */
diff --git a/plugins/openchoreo/src/extensions/entityLayouts/ComponentOverviewLayout.tsx b/plugins/openchoreo/src/extensions/entityLayouts/ComponentOverviewLayout.tsx
index 7d2b04039..190e13e7f 100644
--- a/plugins/openchoreo/src/extensions/entityLayouts/ComponentOverviewLayout.tsx
+++ b/plugins/openchoreo/src/extensions/entityLayouts/ComponentOverviewLayout.tsx
@@ -1,6 +1,9 @@
import Grid from '@material-ui/core/Grid';
import type { EntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha';
-import { FeatureGate } from '@openchoreo/backstage-plugin-react';
+import {
+ BuildFailureNotifierSlot,
+ FeatureGate,
+} from '@openchoreo/backstage-plugin-react';
import {
WorkflowsOverviewCard,
DeploymentStatusCard,
@@ -12,19 +15,17 @@ import { ContainedCatalogGraphCard } from '../../components/ContainedCatalogGrap
import { ForeignCardsSection } from './foreignCards';
/**
- * The Component-kind Overview layout. The portal separately overlays
- * `FailedBuildSnackbar` (private `openchoreo-portal-assistant` plugin,
- * not shipped to adopters) and `WorkflowsOrExternalCICard` (portal-only
- * adapter over Jenkins/GitHub Actions/GitLab) at the app layer; adopters
- * get the plain `WorkflowsOverviewCard` here. Both are opt-in
- * customizations rather than portal defaults, applied via the portal's
- * thin `page:catalog/entity` override.
+ * The Component-kind Overview layout. `BuildFailureNotifierSlot` renders the
+ * host app's assistant prompt for a failed build run (nothing when no
+ * assistant integration is registered — the stock portal's is private and
+ * not shipped to adopters).
*/
export default function ComponentOverviewLayout({
cards,
}: EntityContentLayoutProps) {
return (
+
diff --git a/yarn.lock b/yarn.lock
index 6cb15d7bb..404022f64 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10375,7 +10375,6 @@ __metadata:
"@openchoreo/backstage-plugin-common": "workspace:^"
"@openchoreo/backstage-plugin-openchoreo-ci": "workspace:^"
"@openchoreo/backstage-plugin-openchoreo-observability": "workspace:^"
- "@openchoreo/backstage-plugin-openchoreo-portal-assistant": "workspace:^"
"@openchoreo/backstage-plugin-openchoreo-workflows": "workspace:^"
"@openchoreo/backstage-plugin-platform-engineer-core": "workspace:^"
"@openchoreo/backstage-plugin-react": "workspace:^"
@@ -10484,6 +10483,23 @@ __metadata:
languageName: unknown
linkType: soft
+"@openchoreo/create-portal@workspace:packages/create-portal":
+ version: 0.0.0-use.local
+ resolution: "@openchoreo/create-portal@workspace:packages/create-portal"
+ dependencies:
+ "@backstage/cli": "npm:^0.36.2"
+ "@types/fs-extra": "npm:^11.0.4"
+ "@types/inquirer": "npm:^8.2.10"
+ chalk: "npm:^4.1.2"
+ commander: "npm:^12.1.0"
+ fs-extra: "npm:^11.2.0"
+ handlebars: "npm:^4.7.8"
+ inquirer: "npm:^8.2.6"
+ bin:
+ create-portal: bin/create-portal
+ languageName: unknown
+ linkType: soft
+
"@openchoreo/openapi-client-generator-node@workspace:^, @openchoreo/openapi-client-generator-node@workspace:packages/openapi-client-generator-node":
version: 0.0.0-use.local
resolution: "@openchoreo/openapi-client-generator-node@workspace:packages/openapi-client-generator-node"
@@ -15405,6 +15421,16 @@ __metadata:
languageName: node
linkType: hard
+"@types/fs-extra@npm:^11.0.4":
+ version: 11.0.4
+ resolution: "@types/fs-extra@npm:11.0.4"
+ dependencies:
+ "@types/jsonfile": "npm:*"
+ "@types/node": "npm:*"
+ checksum: 10c0/9e34f9b24ea464f3c0b18c3f8a82aefc36dc524cc720fc2b886e5465abc66486ff4e439ea3fb2c0acebf91f6d3f74e514f9983b1f02d4243706bdbb7511796ad
+ languageName: node
+ linkType: hard
+
"@types/graceful-fs@npm:^4.1.3":
version: 4.1.9
resolution: "@types/graceful-fs@npm:4.1.9"
@@ -15475,6 +15501,16 @@ __metadata:
languageName: node
linkType: hard
+"@types/inquirer@npm:^8.2.10":
+ version: 8.2.13
+ resolution: "@types/inquirer@npm:8.2.13"
+ dependencies:
+ "@types/through": "npm:*"
+ rxjs: "npm:^7.2.0"
+ checksum: 10c0/bfa4fac8c126bce4708418c637f32e39bbf4125869a2351257c9fb3b76d13b96e6e9be279fbcbdf52e94a8dbf5b3dc773275e08b56ab396b6d0314b9cad00163
+ languageName: node
+ linkType: hard
+
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6":
version: 2.0.6
resolution: "@types/istanbul-lib-coverage@npm:2.0.6"
@@ -15556,6 +15592,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/jsonfile@npm:*":
+ version: 6.1.4
+ resolution: "@types/jsonfile@npm:6.1.4"
+ dependencies:
+ "@types/node": "npm:*"
+ checksum: 10c0/b12d068b021e4078f6ac4441353965769be87acf15326173e2aea9f3bf8ead41bd0ad29421df5bbeb0123ec3fc02eb0a734481d52903704a1454a1845896b9eb
+ languageName: node
+ linkType: hard
+
"@types/jsonwebtoken@npm:^9.0.0":
version: 9.0.10
resolution: "@types/jsonwebtoken@npm:9.0.10"
@@ -16072,6 +16117,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/through@npm:*":
+ version: 0.0.33
+ resolution: "@types/through@npm:0.0.33"
+ dependencies:
+ "@types/node": "npm:*"
+ checksum: 10c0/6a8edd7f40cd7e197318e86310a40e568cddd380609dde59b30d5cc6c5f8276ddc698905eac4b3b429eb39f2e8ee326bc20dc6e95a2cdc41c4d3fc9a1ebd4929
+ languageName: node
+ linkType: hard
+
"@types/tough-cookie@npm:*":
version: 4.0.5
resolution: "@types/tough-cookie@npm:4.0.5"
@@ -17075,7 +17129,11 @@ __metadata:
dependencies:
"@axe-core/playwright": "npm:^4.11.3"
"@backstage/cli": "npm:^0.36.2"
+ "@backstage/core-plugin-api": "npm:^1.12.6"
+ "@backstage/frontend-plugin-api": "npm:^0.17.0"
"@backstage/ui": "npm:^0.15.0"
+ "@openchoreo/backstage-plugin-openchoreo-observability": "workspace:^"
+ "@openchoreo/backstage-plugin-openchoreo-portal-assistant": "workspace:^"
"@openchoreo/backstage-portal-app": "workspace:^"
"@playwright/test": "npm:1.56.0"
"@testing-library/dom": "npm:9.3.4"
@@ -19161,7 +19219,7 @@ __metadata:
languageName: node
linkType: hard
-"commander@npm:^12.0.0":
+"commander@npm:^12.0.0, commander@npm:^12.1.0":
version: 12.1.0
resolution: "commander@npm:12.1.0"
checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9
@@ -23939,6 +23997,24 @@ __metadata:
languageName: node
linkType: hard
+"handlebars@npm:^4.7.8":
+ version: 4.7.9
+ resolution: "handlebars@npm:4.7.9"
+ dependencies:
+ minimist: "npm:^1.2.5"
+ neo-async: "npm:^2.6.2"
+ source-map: "npm:^0.6.1"
+ uglify-js: "npm:^3.1.4"
+ wordwrap: "npm:^1.0.0"
+ dependenciesMeta:
+ uglify-js:
+ optional: true
+ bin:
+ handlebars: bin/handlebars
+ checksum: 10c0/22f8105a7e68e81aff2662bb434edf05f757d21d850731d71cec886d69c10cd33d3c43e34b2892968ec62de8241611851d3d0674c8ef324ea3e01dc66262faa9
+ languageName: node
+ linkType: hard
+
"harmony-reflect@npm:^1.4.6":
version: 1.6.2
resolution: "harmony-reflect@npm:1.6.2"
@@ -24923,7 +24999,7 @@ __metadata:
languageName: node
linkType: hard
-"inquirer@npm:^8.2.0":
+"inquirer@npm:^8.2.0, inquirer@npm:^8.2.6":
version: 8.2.7
resolution: "inquirer@npm:8.2.7"
dependencies:
@@ -34293,7 +34369,7 @@ __metadata:
languageName: node
linkType: hard
-"rxjs@npm:^7.5.5":
+"rxjs@npm:^7.2.0, rxjs@npm:^7.5.5":
version: 7.8.2
resolution: "rxjs@npm:7.8.2"
dependencies: