From 67dfd6f9e22c8dadf72819e68e1dca3c41300e49 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 10:55:30 +0530 Subject: [PATCH 01/44] chore(app): declare NFS frontend-system packages as direct deps Pulls @backstage/frontend-defaults, frontend-app-api, frontend-plugin-api, core-compat-api, plugin-app, plugin-app-react forward to their ^0.17.0-line versions so createFrontendPlugin/blueprint imports type-check against a single resolved frontend-plugin-api. Required preparation before NFS plugin conversion. Signed-off-by: Kavith Lokuhewage --- packages/app/package.json | 7 + yarn.lock | 314 ++++++++++++++++++++++++-------------- 2 files changed, 205 insertions(+), 116 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 2dccef85e..4683c09f2 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -26,10 +26,16 @@ "@backstage/cli": "^0.36.2", "@backstage/config": "^1.3.8", "@backstage/core-app-api": "^1.20.1", + "@backstage/core-compat-api": "^0.5.11", "@backstage/core-components": "^0.18.10", "@backstage/core-plugin-api": "^1.12.6", + "@backstage/frontend-app-api": "^0.16.3", + "@backstage/frontend-defaults": "^0.5.2", + "@backstage/frontend-plugin-api": "^0.17.0", "@backstage/integration-react": "^1.2.18", "@backstage/plugin-api-docs": "^0.14.1", + "@backstage/plugin-app": "^0.4.6", + "@backstage/plugin-app-react": "^0.2.3", "@backstage/plugin-catalog": "^2.0.5", "@backstage/plugin-catalog-common": "^1.1.10", "@backstage/plugin-catalog-graph": "^0.6.4", @@ -83,6 +89,7 @@ }, "devDependencies": { "@axe-core/playwright": "^4.11.3", + "@backstage/frontend-test-utils": "^0.6.0", "@backstage/test-utils": "^1.7.18", "@playwright/test": "1.56.0", "@testing-library/dom": "9.3.4", diff --git a/yarn.lock b/yarn.lock index 613b5a613..89e1a17dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1526,19 +1526,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.28.3": - version: 7.28.3 - resolution: "@babel/generator@npm:7.28.3" - dependencies: - "@babel/parser": "npm:^7.28.3" - "@babel/types": "npm:^7.28.2" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" - jsesc: "npm:^3.0.2" - checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc - languageName: node - linkType: hard - "@babel/generator@npm:^7.29.7, @babel/generator@npm:^7.7.2": version: 7.29.7 resolution: "@babel/generator@npm:7.29.7" @@ -1565,13 +1552,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-globals@npm:^7.28.0": - version: 7.28.0 - resolution: "@babel/helper-globals@npm:7.28.0" - checksum: 10c0/5a0cd0c0e8c764b5f27f2095e4243e8af6fa145daea2b41b53c0c1414fe6ff139e3640f4e2207ae2b3d2153a1abd346f901c26c290ee7cb3881dd922d4ee9232 - languageName: node - linkType: hard - "@babel/helper-globals@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-globals@npm:7.29.7" @@ -1579,17 +1559,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7": - version: 7.27.1 - resolution: "@babel/helper-module-imports@npm:7.27.1" - dependencies: - "@babel/traverse": "npm:^7.27.1" - "@babel/types": "npm:^7.27.1" - checksum: 10c0/e00aace096e4e29290ff8648455c2bc4ed982f0d61dbf2db1b5e750b9b98f318bf5788d75a4f974c151bd318fd549e81dbcab595f46b14b81c12eda3023f51e8 - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.29.7": +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-module-imports@npm:7.29.7" dependencies: @@ -1619,13 +1589,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-string-parser@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/helper-string-parser@npm:7.27.1" - checksum: 10c0/8bda3448e07b5583727c103560bcf9c4c24b3c1051a4c516d4050ef69df37bb9a4734a585fe12725b8c2763de0a265aa1e909b485a4e3270b7cfd3e4dbe4b602 - languageName: node - linkType: hard - "@babel/helper-string-parser@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-string-parser@npm:7.29.7" @@ -1633,7 +1596,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1, @babel/helper-validator-identifier@npm:^7.29.7": +"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.29.7": version: 7.29.7 resolution: "@babel/helper-validator-identifier@npm:7.29.7" checksum: 10c0/4795354e7ae0dcafa72de1cd04ec51252dc1498517170beaf019e03effc5b7bf13c6b21a3949a77e07b8125be7f106ed1131350d8ebd4566ae874094a726d62b @@ -1680,17 +1643,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/parser@npm:7.28.4" - dependencies: - "@babel/types": "npm:^7.28.4" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/58b239a5b1477ac7ed7e29d86d675cc81075ca055424eba6485872626db2dc556ce63c45043e5a679cd925e999471dba8a3ed4864e7ab1dbf64306ab72c52707 - languageName: node - linkType: hard - "@babel/plugin-syntax-async-generators@npm:^7.8.4": version: 7.8.4 resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" @@ -1894,17 +1846,6 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.27.2": - version: 7.27.2 - resolution: "@babel/template@npm:7.27.2" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/parser": "npm:^7.27.2" - "@babel/types": "npm:^7.27.1" - checksum: 10c0/ed9e9022651e463cc5f2cc21942f0e74544f1754d231add6348ff1b472985a3b3502041c0be62dc99ed2d12cfae0c51394bf827452b98a2f8769c03b87aadc81 - languageName: node - linkType: hard - "@babel/template@npm:^7.29.7, @babel/template@npm:^7.3.3": version: 7.29.7 resolution: "@babel/template@npm:7.29.7" @@ -1916,21 +1857,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.1": - version: 7.28.4 - resolution: "@babel/traverse@npm:7.28.4" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.3" - "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.4" - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.4" - debug: "npm:^4.3.1" - checksum: 10c0/ee678fdd49c9f54a32e07e8455242390d43ce44887cea6567b233fe13907b89240c377e7633478a32c6cf1be0e17c2f7f3b0c59f0666e39c5074cc47b968489c - languageName: node - linkType: hard - "@babel/traverse@npm:^7.29.7": version: 7.29.7 resolution: "@babel/traverse@npm:7.29.7" @@ -1946,7 +1872,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.29.7, @babel/types@npm:^7.3.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.28.2, @babel/types@npm:^7.29.7, @babel/types@npm:^7.3.3": version: 7.29.7 resolution: "@babel/types@npm:7.29.7" dependencies: @@ -1956,16 +1882,6 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.27.1, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4": - version: 7.28.4 - resolution: "@babel/types@npm:7.28.4" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10c0/ac6f909d6191319e08c80efbfac7bd9a25f80cc83b43cd6d82e7233f7a6b9d6e7b90236f3af7400a3f83b576895bcab9188a22b584eb0f224e80e6d4e95f4517 - languageName: node - linkType: hard - "@backstage-community/plugin-github-actions@npm:^0.20.0": version: 0.20.0 resolution: "@backstage-community/plugin-github-actions@npm:0.20.0" @@ -3163,6 +3079,33 @@ __metadata: languageName: node linkType: hard +"@backstage/frontend-app-api@npm:^0.16.3": + version: 0.16.3 + resolution: "@backstage/frontend-app-api@npm:0.16.3" + dependencies: + "@backstage/config": "npm:^1.3.8" + "@backstage/core-app-api": "npm:^1.20.1" + "@backstage/core-plugin-api": "npm:^1.12.6" + "@backstage/errors": "npm:^1.3.1" + "@backstage/filter-predicates": "npm:^0.1.3" + "@backstage/frontend-defaults": "npm:^0.5.2" + "@backstage/frontend-plugin-api": "npm:^0.17.0" + "@backstage/types": "npm:^1.2.2" + "@backstage/version-bridge": "npm:^1.0.12" + lodash: "npm:^4.17.21" + zod: "npm:^3.25.76 || ^4.0.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.30.2 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/1ff0c0708193aa1109acd61f428f783f7fec7c846b3f0ce9def15f9f112d7cfb00da1ad4665a977d6f700db9a31e9a66d75c8c6b991342c43135d06eb66684ef + languageName: node + linkType: hard + "@backstage/frontend-defaults@npm:^0.3.6": version: 0.3.6 resolution: "@backstage/frontend-defaults@npm:0.3.6" @@ -3186,6 +3129,29 @@ __metadata: languageName: node linkType: hard +"@backstage/frontend-defaults@npm:^0.5.2": + version: 0.5.2 + resolution: "@backstage/frontend-defaults@npm:0.5.2" + dependencies: + "@backstage/config": "npm:^1.3.8" + "@backstage/core-components": "npm:^0.18.10" + "@backstage/errors": "npm:^1.3.1" + "@backstage/frontend-app-api": "npm:^0.16.3" + "@backstage/frontend-plugin-api": "npm:^0.17.0" + "@backstage/plugin-app": "npm:^0.4.6" + "@react-hookz/web": "npm:^24.0.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.30.2 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/81c558200ae3dff3472ca974325dbb2147266389360120dcf5a8c575320f5fe295ac237a39ed5cb58ca94d77328604f3ebe4a1a58f99fb451edf216e4a7d3dfe + languageName: node + linkType: hard + "@backstage/frontend-plugin-api@npm:^0.13.3, @backstage/frontend-plugin-api@npm:^0.13.4": version: 0.13.4 resolution: "@backstage/frontend-plugin-api@npm:0.13.4" @@ -3256,6 +3222,42 @@ __metadata: languageName: node linkType: hard +"@backstage/frontend-test-utils@npm:^0.6.0": + version: 0.6.0 + resolution: "@backstage/frontend-test-utils@npm:0.6.0" + dependencies: + "@backstage/config": "npm:^1.3.8" + "@backstage/core-app-api": "npm:^1.20.1" + "@backstage/core-plugin-api": "npm:^1.12.6" + "@backstage/filter-predicates": "npm:^0.1.3" + "@backstage/frontend-app-api": "npm:^0.16.3" + "@backstage/frontend-plugin-api": "npm:^0.17.0" + "@backstage/plugin-app": "npm:^0.4.6" + "@backstage/plugin-app-react": "npm:^0.2.3" + "@backstage/plugin-permission-common": "npm:^0.9.9" + "@backstage/plugin-permission-react": "npm:^0.5.1" + "@backstage/test-utils": "npm:^1.7.18" + "@backstage/types": "npm:^1.2.2" + "@backstage/version-bridge": "npm:^1.0.12" + i18next: "npm:^22.4.15" + zen-observable: "npm:^0.10.0" + zod: "npm:^3.25.76 || ^4.0.0" + peerDependencies: + "@testing-library/react": ^16.0.0 + "@types/jest": "*" + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.30.2 + peerDependenciesMeta: + "@types/jest": + optional: true + "@types/react": + optional: true + checksum: 10c0/022f3e43d0373923f2d363bbdeeed70789a4d6b8464a3f92a2a89ac6f007f0c7d7f140fc04484ae81c44f38251d73a9e92b591d505cdfbf209cb1051530e803f + languageName: node + linkType: hard + "@backstage/integration-aws-node@npm:^0.1.19": version: 0.1.19 resolution: "@backstage/integration-aws-node@npm:0.1.19" @@ -3509,6 +3511,44 @@ __metadata: languageName: node linkType: hard +"@backstage/plugin-app@npm:^0.4.6": + version: 0.4.6 + resolution: "@backstage/plugin-app@npm:0.4.6" + dependencies: + "@backstage/core-components": "npm:^0.18.10" + "@backstage/core-plugin-api": "npm:^1.12.6" + "@backstage/filter-predicates": "npm:^0.1.3" + "@backstage/frontend-plugin-api": "npm:^0.17.0" + "@backstage/integration-react": "npm:^1.2.18" + "@backstage/plugin-app-react": "npm:^0.2.3" + "@backstage/plugin-permission-react": "npm:^0.5.1" + "@backstage/theme": "npm:^0.7.3" + "@backstage/types": "npm:^1.2.2" + "@backstage/ui": "npm:^0.15.0" + "@backstage/version-bridge": "npm:^1.0.12" + "@material-ui/core": "npm:^4.9.13" + "@material-ui/icons": "npm:^4.9.1" + "@material-ui/lab": "npm:^4.0.0-alpha.61" + "@react-hookz/web": "npm:^24.0.0" + "@remixicon/react": "npm:>=4.6.0 <4.9.0" + motion: "npm:^12.0.0" + react-aria: "npm:~3.48.0" + react-stately: "npm:~3.46.0" + react-use: "npm:^17.2.4" + zen-observable: "npm:^0.10.0" + zod: "npm:^4.0.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.30.2 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/9be399bb67c1e7551f60c3fffa50419a79d968d51d69a29d489355814b8327745f4cd50510bae710ba6bee0fc197a3a91b9c523469ee09d9aef5c00cfa090f96 + languageName: node + linkType: hard + "@backstage/plugin-auth-backend-module-github-provider@npm:^0.5.3": version: 0.5.3 resolution: "@backstage/plugin-auth-backend-module-github-provider@npm:0.5.3" @@ -14981,7 +15021,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.2": +"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.3.4": version: 8.3.5 resolution: "acorn-walk@npm:8.3.5" dependencies: @@ -14990,15 +15030,6 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.3.4": - version: 8.3.4 - resolution: "acorn-walk@npm:8.3.4" - dependencies: - acorn: "npm:^8.11.0" - checksum: 10c0/76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 - languageName: node - linkType: hard - "acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.14.1, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1, acorn@npm:^8.9.0": version: 8.16.0 resolution: "acorn@npm:8.16.0" @@ -15303,10 +15334,17 @@ __metadata: "@backstage/cli": "npm:^0.36.2" "@backstage/config": "npm:^1.3.8" "@backstage/core-app-api": "npm:^1.20.1" + "@backstage/core-compat-api": "npm:^0.5.11" "@backstage/core-components": "npm:^0.18.10" "@backstage/core-plugin-api": "npm:^1.12.6" + "@backstage/frontend-app-api": "npm:^0.16.3" + "@backstage/frontend-defaults": "npm:^0.5.2" + "@backstage/frontend-plugin-api": "npm:^0.17.0" + "@backstage/frontend-test-utils": "npm:^0.6.0" "@backstage/integration-react": "npm:^1.2.18" "@backstage/plugin-api-docs": "npm:^0.14.1" + "@backstage/plugin-app": "npm:^0.4.6" + "@backstage/plugin-app-react": "npm:^0.2.3" "@backstage/plugin-catalog": "npm:^2.0.5" "@backstage/plugin-catalog-common": "npm:^1.1.10" "@backstage/plugin-catalog-graph": "npm:^0.6.4" @@ -21080,6 +21118,28 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^12.40.0": + version: 12.40.0 + resolution: "framer-motion@npm:12.40.0" + dependencies: + motion-dom: "npm:^12.40.0" + motion-utils: "npm:^12.39.0" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/a1d26908d6661028fcdba0cf200fca18927a4d4eae0b1e64c37dfb7fdea9da66a8991abd0007079e98687060ba9c83db55620c238bc363106a24ff411d22f533 + languageName: node + linkType: hard + "framer-motion@npm:^6.5.1": version: 6.5.1 resolution: "framer-motion@npm:6.5.1" @@ -27210,6 +27270,43 @@ __metadata: languageName: node linkType: hard +"motion-dom@npm:^12.40.0": + version: 12.40.0 + resolution: "motion-dom@npm:12.40.0" + dependencies: + motion-utils: "npm:^12.39.0" + checksum: 10c0/79da846a36fd5f6762a0fcfa6e0b7128e4d58f7c07d1467a9f789a9cd0b5adbef9bfbde75760901a13cf2bddd9b31e93e4348a714f570c45ca1e2bfabd22859e + languageName: node + linkType: hard + +"motion-utils@npm:^12.39.0": + version: 12.39.0 + resolution: "motion-utils@npm:12.39.0" + checksum: 10c0/6d7a2a2cc0797b72410a666a9cc1c201c8e39bf9669670464e433fe1e72af5f0217154c869867b34fbadf3664cf222c0d022bbc4eed7927f201ae971918e7440 + languageName: node + linkType: hard + +"motion@npm:^12.0.0": + version: 12.40.0 + resolution: "motion@npm:12.40.0" + dependencies: + framer-motion: "npm:^12.40.0" + tslib: "npm:^2.4.0" + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 10c0/d0c118ed4829f2999c3ab7eb1ee916df70c65e95d262e951b69d3cea67a74e4a1d12e181badf4180e0d01c1ca8a9b109be4ea5456cad04bb58d4510f071af60f + languageName: node + linkType: hard + "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4" @@ -35906,9 +36003,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.8.0": - version: 8.18.3 - resolution: "ws@npm:8.18.3" +"ws@npm:*, ws@npm:^8.11.0, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.8.0": + version: 8.21.0 + resolution: "ws@npm:8.21.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -35917,7 +36014,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 languageName: node linkType: hard @@ -35936,21 +36033,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0": - version: 8.21.0 - resolution: "ws@npm:8.21.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 - languageName: node - linkType: hard - "wsl-utils@npm:^0.1.0": version: 0.1.0 resolution: "wsl-utils@npm:0.1.0" From 50d8c878bca828ee5a0cda6866873f3db3877b74 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 11:43:05 +0530 Subject: [PATCH 02/44] feat(openchoreo-ci): add /alpha NFS entry point Expose the plugin via @backstage/frontend-plugin-api's createFrontendPlugin in src/alpha.tsx so the NFS app shell can consume it directly. The legacy src/plugin.ts and dev sandbox continue to work unchanged. Mirrors the upstream pattern used by @backstage/plugin-api-docs and friends. Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo-ci/package.json | 17 ++++++++++++ plugins/openchoreo-ci/src/alpha.tsx | 40 +++++++++++++++++++++++++++++ yarn.lock | 1 + 3 files changed, 58 insertions(+) create mode 100644 plugins/openchoreo-ci/src/alpha.tsx diff --git a/plugins/openchoreo-ci/package.json b/plugins/openchoreo-ci/package.json index 6c2b167b5..b7c9cd8d3 100644 --- a/plugins/openchoreo-ci/package.json +++ b/plugins/openchoreo-ci/package.json @@ -4,10 +4,26 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } + }, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts", + "alpha": "dist/alpha.esm.js", "registry": "https://npm.pkg.github.com" }, "repository": { @@ -36,6 +52,7 @@ "@backstage/catalog-model": "^1.9.0", "@backstage/core-components": "^0.18.10", "@backstage/core-plugin-api": "^1.12.6", + "@backstage/frontend-plugin-api": "^0.17.0", "@backstage/plugin-catalog-react": "^3.0.0", "@backstage/theme": "^0.7.3", "@material-ui/core": "4.12.4", diff --git a/plugins/openchoreo-ci/src/alpha.tsx b/plugins/openchoreo-ci/src/alpha.tsx new file mode 100644 index 000000000..bae98d861 --- /dev/null +++ b/plugins/openchoreo-ci/src/alpha.tsx @@ -0,0 +1,40 @@ +import { + ApiBlueprint, + createFrontendPlugin, + discoveryApiRef, + fetchApiRef, +} from '@backstage/frontend-plugin-api'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +import { rootRouteRef } from './routes'; +import { openChoreoCiClientApiRef } from './api/OpenChoreoCiClientApi'; +import { OpenChoreoCiClient } from './api/OpenChoreoCiClient'; + +const ciClientApi = ApiBlueprint.make({ + name: 'open-choreo-ci-client', + params: defineParams => + defineParams({ + api: openChoreoCiClientApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new OpenChoreoCiClient(discoveryApi, fetchApi), + }), +}); + +const workflowsEntityContent = EntityContentBlueprint.make({ + name: 'workflows', + params: { + path: '/workflows', + title: 'Build', + loader: () => import('./components/Workflows').then(m => ), + }, +}); + +/** + * NFS entry point for the OpenChoreo CI plugin. + */ +export default createFrontendPlugin({ + pluginId: 'openchoreo-ci', + routes: { root: rootRouteRef }, + extensions: [ciClientApi, workflowsEntityContent], +}); diff --git a/yarn.lock b/yarn.lock index 89e1a17dd..8a2239a84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9035,6 +9035,7 @@ __metadata: "@backstage/core-components": "npm:^0.18.10" "@backstage/core-plugin-api": "npm:^1.12.6" "@backstage/dev-utils": "npm:^1.1.23" + "@backstage/frontend-plugin-api": "npm:^0.17.0" "@backstage/plugin-catalog-react": "npm:^3.0.0" "@backstage/test-utils": "npm:^1.7.18" "@backstage/theme": "npm:^0.7.3" From 32a1d082003045b4cd11cbef0fc0f120cd572573 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 12:01:08 +0530 Subject: [PATCH 03/44] feat(openchoreo-workflows): add /alpha NFS entry point Expose the plugin as a createFrontendPlugin with the GenericWorkflowsClient API and the GenericWorkflowsPage routable extension translated to a PageBlueprint. Legacy src/plugin.ts remains unchanged. Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo-workflows/package.json | 17 +++++++++ plugins/openchoreo-workflows/src/alpha.tsx | 43 ++++++++++++++++++++++ yarn.lock | 1 + 3 files changed, 61 insertions(+) create mode 100644 plugins/openchoreo-workflows/src/alpha.tsx diff --git a/plugins/openchoreo-workflows/package.json b/plugins/openchoreo-workflows/package.json index bdf86d713..2cd5fd5e2 100644 --- a/plugins/openchoreo-workflows/package.json +++ b/plugins/openchoreo-workflows/package.json @@ -4,10 +4,26 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } + }, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts", + "alpha": "dist/alpha.esm.js", "registry": "https://npm.pkg.github.com" }, "repository": { @@ -34,6 +50,7 @@ "dependencies": { "@backstage/core-components": "^0.18.10", "@backstage/core-plugin-api": "^1.12.6", + "@backstage/frontend-plugin-api": "^0.17.0", "@backstage/plugin-catalog-react": "^3.0.0", "@backstage/theme": "^0.7.3", "@material-ui/core": "4.12.4", diff --git a/plugins/openchoreo-workflows/src/alpha.tsx b/plugins/openchoreo-workflows/src/alpha.tsx new file mode 100644 index 000000000..3e9b02612 --- /dev/null +++ b/plugins/openchoreo-workflows/src/alpha.tsx @@ -0,0 +1,43 @@ +import { + ApiBlueprint, + createFrontendPlugin, + discoveryApiRef, + fetchApiRef, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; + +import { rootRouteRef } from './routes'; +import { genericWorkflowsClientApiRef } from './api/GenericWorkflowsClientApi'; +import { GenericWorkflowsClient } from './api/GenericWorkflowsClient'; + +const genericWorkflowsClientApi = ApiBlueprint.make({ + name: 'generic-workflows-client', + params: defineParams => + defineParams({ + api: genericWorkflowsClientApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new GenericWorkflowsClient(discoveryApi, fetchApi), + }), +}); + +const genericWorkflowsPage = PageBlueprint.make({ + name: 'generic-workflows', + params: { + path: '/workflows', + routeRef: rootRouteRef, + loader: () => + import('./components/GenericWorkflowsPage').then(m => ( + + )), + }, +}); + +/** + * NFS entry point for the OpenChoreo generic-workflows plugin. + */ +export default createFrontendPlugin({ + pluginId: 'openchoreo-workflows', + routes: { root: rootRouteRef }, + extensions: [genericWorkflowsClientApi, genericWorkflowsPage], +}); diff --git a/yarn.lock b/yarn.lock index 8a2239a84..7c3b04d9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9195,6 +9195,7 @@ __metadata: "@backstage/core-components": "npm:^0.18.10" "@backstage/core-plugin-api": "npm:^1.12.6" "@backstage/dev-utils": "npm:^1.1.23" + "@backstage/frontend-plugin-api": "npm:^0.17.0" "@backstage/plugin-catalog-react": "npm:^3.0.0" "@backstage/test-utils": "npm:^1.7.18" "@backstage/theme": "npm:^0.7.3" From 3845410faf0a5abce857e0ad62017c00c97265c8 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 12:06:17 +0530 Subject: [PATCH 04/44] feat(platform-engineer-core): add /alpha NFS entry point Expose the plugin as a createFrontendPlugin with the PlatformEngineerDashboardView translated to a PageBlueprint. Legacy src/plugin.ts and dev sandbox unchanged. Signed-off-by: Kavith Lokuhewage --- plugins/platform-engineer-core/package.json | 17 ++++++++++++ plugins/platform-engineer-core/src/alpha.tsx | 27 ++++++++++++++++++++ yarn.lock | 1 + 3 files changed, 45 insertions(+) create mode 100644 plugins/platform-engineer-core/src/alpha.tsx diff --git a/plugins/platform-engineer-core/package.json b/plugins/platform-engineer-core/package.json index bfd8daa42..f28e68b16 100644 --- a/plugins/platform-engineer-core/package.json +++ b/plugins/platform-engineer-core/package.json @@ -4,6 +4,21 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/openchoreo/backstage-plugins.git", @@ -13,6 +28,7 @@ "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts", + "alpha": "dist/alpha.esm.js", "registry": "https://npm.pkg.github.com" }, "backstage": { @@ -37,6 +53,7 @@ "@backstage/catalog-model": "^1.9.0", "@backstage/core-components": "^0.18.10", "@backstage/core-plugin-api": "^1.12.6", + "@backstage/frontend-plugin-api": "^0.17.0", "@backstage/plugin-catalog": "^2.0.5", "@backstage/plugin-catalog-react": "^3.0.0", "@backstage/theme": "^0.7.3", diff --git a/plugins/platform-engineer-core/src/alpha.tsx b/plugins/platform-engineer-core/src/alpha.tsx new file mode 100644 index 000000000..4b96bd435 --- /dev/null +++ b/plugins/platform-engineer-core/src/alpha.tsx @@ -0,0 +1,27 @@ +import { + createFrontendPlugin, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; + +import { rootRouteRef } from './routes'; + +const platformEngineerDashboardPage = PageBlueprint.make({ + name: 'platform-engineer-dashboard', + params: { + path: '/platform-engineer-view', + routeRef: rootRouteRef, + loader: () => + import('./views/PlatformEngineerDashboardView').then(m => ( + + )), + }, +}); + +/** + * NFS entry point for the Platform Engineer Core plugin. + */ +export default createFrontendPlugin({ + pluginId: 'platform-engineer-core', + routes: { root: rootRouteRef }, + extensions: [platformEngineerDashboardPage], +}); diff --git a/yarn.lock b/yarn.lock index 7c3b04d9d..c14188be9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9281,6 +9281,7 @@ __metadata: "@backstage/core-components": "npm:^0.18.10" "@backstage/core-plugin-api": "npm:^1.12.6" "@backstage/dev-utils": "npm:^1.1.23" + "@backstage/frontend-plugin-api": "npm:^0.17.0" "@backstage/plugin-catalog": "npm:^2.0.5" "@backstage/plugin-catalog-react": "npm:^3.0.0" "@backstage/test-utils": "npm:^1.7.18" From b17fde474400ea3fa865a0e2ffbe614a88e2c5f0 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 12:08:17 +0530 Subject: [PATCH 05/44] feat(openchoreo-observability): add /alpha NFS entry point Register the three observability backend APIs (observability, RCA agent, FinOps agent) as ApiBlueprint extensions. The nine legacy routable extensions (Metrics, Traces, RCA, Logs, Alerts, Wirelogs, Incidents, CostAnalysis) keep flowing through src/plugin.ts because the host app mounts them with per- mount props inside legacy EntityLayout.Route blocks. Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo-observability/package.json | 17 +++++ .../openchoreo-observability/src/alpha.tsx | 64 +++++++++++++++++++ yarn.lock | 1 + 3 files changed, 82 insertions(+) create mode 100644 plugins/openchoreo-observability/src/alpha.tsx diff --git a/plugins/openchoreo-observability/package.json b/plugins/openchoreo-observability/package.json index b2bf2be6c..61d6ea26d 100644 --- a/plugins/openchoreo-observability/package.json +++ b/plugins/openchoreo-observability/package.json @@ -4,10 +4,26 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } + }, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts", + "alpha": "dist/alpha.esm.js", "registry": "https://npm.pkg.github.com" }, "repository": { @@ -36,6 +52,7 @@ "@backstage/catalog-model": "^1.9.0", "@backstage/core-components": "^0.18.10", "@backstage/core-plugin-api": "^1.12.6", + "@backstage/frontend-plugin-api": "^0.17.0", "@backstage/plugin-catalog-react": "^3.0.0", "@backstage/theme": "^0.7.3", "@date-io/date-fns": "1.3.13", diff --git a/plugins/openchoreo-observability/src/alpha.tsx b/plugins/openchoreo-observability/src/alpha.tsx new file mode 100644 index 000000000..a1094d62a --- /dev/null +++ b/plugins/openchoreo-observability/src/alpha.tsx @@ -0,0 +1,64 @@ +import { + ApiBlueprint, + createFrontendPlugin, + discoveryApiRef, + fetchApiRef, +} from '@backstage/frontend-plugin-api'; + +import { rootRouteRef } from './routes'; +import { + observabilityApiRef, + ObservabilityClient, +} from './api/ObservabilityApi'; +import { rcaAgentApiRef, RCAAgentClient } from './api/RCAAgentApi'; +import { finopsAgentApiRef, FinOpsAgentClient } from './api/FinOpsAgentApi'; + +const observabilityApi = ApiBlueprint.make({ + name: 'observability', + params: defineParams => + defineParams({ + api: observabilityApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new ObservabilityClient({ discoveryApi, fetchApi }), + }), +}); + +const rcaAgentApi = ApiBlueprint.make({ + name: 'rca-agent', + params: defineParams => + defineParams({ + api: rcaAgentApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new RCAAgentClient({ discoveryApi, fetchApi }), + }), +}); + +const finopsAgentApi = ApiBlueprint.make({ + name: 'finops-agent', + params: defineParams => + defineParams({ + api: finopsAgentApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new FinOpsAgentClient({ discoveryApi, fetchApi }), + }), +}); + +/** + * NFS entry point for the OpenChoreo Observability plugin. + * + * Registers the three observability backend clients. The legacy + * `createRoutableExtension` page components (ObservabilityMetrics, + * ObservabilityTraces, …) continue to flow through `src/plugin.ts` and are + * mounted manually by the host app via `` blocks so they + * can receive per-mount props (e.g. `renderRowAction`). When the host moves + * those mounts to NFS-driven entity tabs, additional EntityContentBlueprint + * extensions can be added here. + */ +export default createFrontendPlugin({ + pluginId: 'openchoreo-observability', + routes: { root: rootRouteRef }, + extensions: [observabilityApi, rcaAgentApi, finopsAgentApi], +}); diff --git a/yarn.lock b/yarn.lock index c14188be9..f371b5786 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9097,6 +9097,7 @@ __metadata: "@backstage/core-components": "npm:^0.18.10" "@backstage/core-plugin-api": "npm:^1.12.6" "@backstage/dev-utils": "npm:^1.1.23" + "@backstage/frontend-plugin-api": "npm:^0.17.0" "@backstage/plugin-catalog-react": "npm:^3.0.0" "@backstage/test-utils": "npm:^1.7.18" "@backstage/theme": "npm:^0.7.3" From 36edb0ab9220cabdbd2108b9c014731ce4802de2 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 12:14:19 +0530 Subject: [PATCH 06/44] feat(openchoreo): add /alpha NFS entry point Register the OpenChoreoClient API as an ApiBlueprint extension and expose the plugin's three route refs (catalogEnvironment, accessControl, resourceEnvironments). The four legacy routable extensions and four component cards continue to flow through src/plugin.ts; the host app mounts them as plain React components inside EntityLayout. Step 2 is now complete for all five custom plugins. Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo/package.json | 17 ++++++++++++ plugins/openchoreo/src/alpha.tsx | 47 ++++++++++++++++++++++++++++++++ yarn.lock | 1 + 3 files changed, 65 insertions(+) create mode 100644 plugins/openchoreo/src/alpha.tsx diff --git a/plugins/openchoreo/package.json b/plugins/openchoreo/package.json index 5d579e5d4..f5b215dd3 100644 --- a/plugins/openchoreo/package.json +++ b/plugins/openchoreo/package.json @@ -4,6 +4,21 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/openchoreo/backstage-plugins.git", @@ -13,6 +28,7 @@ "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts", + "alpha": "dist/alpha.esm.js", "registry": "https://npm.pkg.github.com" }, "backstage": { @@ -37,6 +53,7 @@ "@backstage/core-components": "^0.18.10", "@backstage/core-plugin-api": "^1.12.6", "@backstage/errors": "^1.3.1", + "@backstage/frontend-plugin-api": "^0.17.0", "@backstage/plugin-catalog-react": "^3.0.0", "@backstage/plugin-permission-react": "^0.5.1", "@backstage/theme": "^0.7.3", diff --git a/plugins/openchoreo/src/alpha.tsx b/plugins/openchoreo/src/alpha.tsx new file mode 100644 index 000000000..53fea616a --- /dev/null +++ b/plugins/openchoreo/src/alpha.tsx @@ -0,0 +1,47 @@ +import { + ApiBlueprint, + createFrontendPlugin, + discoveryApiRef, + fetchApiRef, +} from '@backstage/frontend-plugin-api'; + +import { + rootCatalogEnvironmentRouteRef, + accessControlRouteRef, + resourceEnvironmentsRouteRef, +} from './routes'; +import { openChoreoClientApiRef } from './api/OpenChoreoClientApi'; +import { OpenChoreoClient } from './api/OpenChoreoClient'; + +const openChoreoClientApi = ApiBlueprint.make({ + name: 'open-choreo-client', + params: defineParams => + defineParams({ + api: openChoreoClientApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new OpenChoreoClient(discoveryApi, fetchApi), + }), +}); + +/** + * NFS entry point for the OpenChoreo plugin. + * + * Registers the OpenChoreoClient API. The four legacy routable extensions + * (Environments, ResourceEnvironments, CellDiagram, AccessControlPage) and + * four component cards (WorkflowsOverviewCard, DeploymentStatusCard, + * RuntimeHealthCard, DeploymentPipelineCard) continue to flow through + * src/plugin.ts because the host app mounts them as plain React components + * inside legacy EntityLayout.Route/Card structures. Wire them as + * EntityContentBlueprint / EntityCardBlueprint extensions here when the host + * moves to NFS-driven entity tabs. + */ +export default createFrontendPlugin({ + pluginId: 'openchoreo', + routes: { + catalogEnvironment: rootCatalogEnvironmentRouteRef, + accessControl: accessControlRouteRef, + resourceEnvironments: resourceEnvironmentsRouteRef, + }, + extensions: [openChoreoClientApi], +}); diff --git a/yarn.lock b/yarn.lock index f371b5786..d103f3b4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9404,6 +9404,7 @@ __metadata: "@backstage/core-plugin-api": "npm:^1.12.6" "@backstage/dev-utils": "npm:^1.1.23" "@backstage/errors": "npm:^1.3.1" + "@backstage/frontend-plugin-api": "npm:^0.17.0" "@backstage/plugin-catalog-react": "npm:^3.0.0" "@backstage/plugin-permission-react": "npm:^0.5.1" "@backstage/test-utils": "npm:^1.7.18" From 8ee4a01a14ad3b48b9b6c144e8f9110adc68cc67 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 13:42:44 +0530 Subject: [PATCH 07/44] feat(app): switch shell to NFS createApp from @backstage/frontend-defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3a of the NFS migration: - App.tsx swaps `createApp` from `@backstage/app-defaults` for the NFS one from `@backstage/frontend-defaults`. Existing apis/icons/components.SignInPage/ themes flow through `convertLegacyAppOptions`; the entire FlatRoutes JSX tree including AppRouter and Root flows through `convertLegacyAppRoot`. - index.tsx and App.test.tsx switch from `` to rendering the JSX value `app` directly, matching the upstream NFS reference app. - Two `app`-scoped API factories (`catalogGraphApiRef`, `entityPresentationApiRef`) are temporarily disabled — under NFS they collide with the upstream plugins that already own those API ids. The proper plugin-scoped overrides land in Step 3c. - `__experimentalTranslations` (catalog-import strings) is deferred to 3c as a TranslationBlueprint module. Dev server boots clean, sign-in + home page verified live. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.test.tsx | 4 +- packages/app/src/App.tsx | 74 ++++++++++----------- packages/app/src/apis.ts | 119 ++++------------------------------ packages/app/src/index.tsx | 4 +- 4 files changed, 51 insertions(+), 150 deletions(-) diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx index b6ca21d42..1ab4a1a63 100644 --- a/packages/app/src/App.test.tsx +++ b/packages/app/src/App.test.tsx @@ -1,5 +1,5 @@ import { render, waitFor } from '@testing-library/react'; -import App from './App'; +import app from './App'; describe('App', () => { it('should render', async () => { @@ -19,7 +19,7 @@ describe('App', () => { ] as any, }; - const rendered = render(); + const rendered = render(app); await waitFor(() => { expect(rendered.baseElement).toBeInTheDocument(); diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index e3302cd8f..d8b1b685b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -9,8 +9,6 @@ import { CatalogImportPage, catalogImportPlugin, } from '@backstage/plugin-catalog-import'; -import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; -import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react'; import { ComponentNamePickerFieldExtension } from './scaffolder/ComponentNamePicker'; @@ -69,7 +67,11 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; -import { createApp } from '@backstage/app-defaults'; +import { createApp } from '@backstage/frontend-defaults'; +import { + convertLegacyAppOptions, + convertLegacyAppRoot, +} from '@backstage/core-compat-api'; import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; @@ -136,23 +138,12 @@ function DynamicSignInPage(props: any) { ); } -const catalogImportTranslations = createTranslationMessages({ - ref: catalogImportTranslationRef, - full: false, - messages: { - 'defaultImportPage.headerTitle': 'Register an existing catalog entity', - 'defaultImportPage.contentHeaderTitle': - 'Start tracking your entity in {{appTitle}}', - 'defaultImportPage.supportTitle': - 'Start tracking your entity in {{appTitle}} by adding it to the software catalog.', - 'importInfoCard.title': 'Register an existing catalog entity', - 'stepInitAnalyzeUrl.urlHelperText': - 'Enter the full path to your entity file to start tracking', - 'stepFinishImportLocation.locations.viewButtonText': 'View Entity', - }, -}); +// NOTE: catalog-import translation overrides (previously passed via +// `__experimentalTranslations`) are deferred to Step 3c — they'll move into a +// `TranslationBlueprint.make({...})` module. Until then, catalog-import shows +// upstream's default header strings. -const app = createApp({ +const legacyAppOptions = convertLegacyAppOptions({ apis, icons: { 'kind:environment': CloudIcon, @@ -174,30 +165,10 @@ const app = createApp({ 'kind:clusterworkflow': PlayCircleOutlineIcon, 'kind:componentworkflow': SettingsApplicationsIcon, }, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - createFromTemplate: scaffolderPlugin.routes.selectedTemplate, - }); - bind(apiDocsPlugin.externalRoutes, { - registerApi: catalogImportPlugin.routes.importPage, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(orgPlugin.externalRoutes, { - catalogIndex: catalogPlugin.routes.catalogIndex, - }); - }, components: { SignInPage: DynamicSignInPage, }, themes: appThemes, - __experimentalTranslations: { - resources: [catalogImportTranslations], - }, }); const routes = ( @@ -305,7 +276,7 @@ const routes = ( ); -export default app.createRoot( +const legacyRoot = convertLegacyAppRoot( <> @@ -316,3 +287,26 @@ export default app.createRoot( , ); + +const app = createApp({ + features: [legacyAppOptions, ...legacyRoot], + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + createFromTemplate: scaffolderPlugin.routes.selectedTemplate, + }); + bind(apiDocsPlugin.externalRoutes, { + registerApi: catalogImportPlugin.routes.importPage, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(orgPlugin.externalRoutes, { + catalogIndex: catalogPlugin.routes.catalogIndex, + }); + }, +}); + +export default app.createRoot(); diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 1e2d6807a..e1de52867 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -52,45 +52,6 @@ import { perchAgentApiRef, PerchAgentClient, } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; -import { - catalogApiRef, - entityPresentationApiRef, -} from '@backstage/plugin-catalog-react'; -import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; -import { - catalogGraphApiRef, - DefaultCatalogGraphApi, - ALL_RELATIONS, - ALL_RELATION_PAIRS, -} from '@backstage/plugin-catalog-graph'; -import { - RELATION_DEPLOYS_TO, - RELATION_DEPLOYED_BY, - RELATION_USES_PIPELINE, - RELATION_PIPELINE_USED_BY, - RELATION_HOSTED_ON, - RELATION_HOSTS, - RELATION_OBSERVED_BY, - RELATION_OBSERVES, - RELATION_INSTANCE_OF, - RELATION_HAS_INSTANCE, - RELATION_USES_WORKFLOW, - RELATION_WORKFLOW_USED_BY, - RELATION_BUILDS_ON, - RELATION_BUILDS, -} from '@openchoreo/backstage-plugin-common'; -import CloudIcon from '@material-ui/icons/Cloud'; -import DnsIcon from '@material-ui/icons/Dns'; -import AccountTreeIcon from '@material-ui/icons/AccountTree'; -import VisibilityIcon from '@material-ui/icons/Visibility'; -import BuildIcon from '@material-ui/icons/Build'; -import CategoryIcon from '@material-ui/icons/Category'; -import LayersIcon from '@material-ui/icons/Layers'; -import StorageIcon from '@material-ui/icons/Storage'; -import ExtensionIcon from '@material-ui/icons/Extension'; -import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; -import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; - // Re-export for use by App.tsx and other components export { openChoreoAuthApiRef }; @@ -201,44 +162,14 @@ export const apis: AnyApiFactory[] = [ new OpenChoreoCiClient(discoveryApi, fetchApi), }), - // Catalog graph API with custom OpenChoreo relations - // Without this, custom relations (deploysTo, hostedOn, instanceOf, etc.) - // won't appear in entity Relations cards or the catalog graph - createApiFactory({ - api: catalogGraphApiRef, - deps: {}, - factory: () => - new DefaultCatalogGraphApi({ - knownRelations: [ - ...ALL_RELATIONS, - RELATION_DEPLOYS_TO, - RELATION_DEPLOYED_BY, - RELATION_USES_PIPELINE, - RELATION_PIPELINE_USED_BY, - RELATION_HOSTED_ON, - RELATION_HOSTS, - RELATION_OBSERVED_BY, - RELATION_OBSERVES, - RELATION_INSTANCE_OF, - RELATION_HAS_INSTANCE, - RELATION_USES_WORKFLOW, - RELATION_WORKFLOW_USED_BY, - RELATION_BUILDS_ON, - RELATION_BUILDS, - ], - knownRelationPairs: [ - ...ALL_RELATION_PAIRS, - [RELATION_DEPLOYS_TO, RELATION_DEPLOYED_BY], - [RELATION_USES_PIPELINE, RELATION_PIPELINE_USED_BY], - [RELATION_HOSTED_ON, RELATION_HOSTS], - [RELATION_OBSERVED_BY, RELATION_OBSERVES], - [RELATION_INSTANCE_OF, RELATION_HAS_INSTANCE], - [RELATION_USES_WORKFLOW, RELATION_WORKFLOW_USED_BY], - [RELATION_BUILDS_ON, RELATION_BUILDS], - ], - defaultRelationTypes: { exclude: [] }, - }), - }), + // DEFERRED to Step 3c: Catalog graph API override with custom OpenChoreo + // relations. The legacy `app`-scoped registration of `catalogGraphApiRef` + // collides with `@backstage/plugin-catalog-graph`'s own default factory + // under NFS (API_FACTORY_CONFLICT). The proper fix is a plugin override + // (catalogGraphPlugin.withOverrides) that disables the upstream default + // and provides our augmented one under the same pluginId. Until then, + // custom relations (deploysTo, hostedOn, instanceOf, …) won't render in + // entity Relations cards or the catalog graph. // Generic Workflows client - provides API for org-level workflow operations createApiFactory({ @@ -264,33 +195,9 @@ export const apis: AnyApiFactory[] = [ new PerchAgentClient({ discoveryApi, fetchApi }), }), - // Custom EntityPresentationApi with icons for custom entity kinds - // This enables icons for Environment, DataPlane, and DeploymentPipeline in the catalog graph - createApiFactory({ - api: entityPresentationApiRef, - deps: { catalogApi: catalogApiRef }, - factory: ({ catalogApi }) => - DefaultEntityPresentationApi.create({ - catalogApi, - kindIcons: { - environment: CloudIcon, - dataplane: DnsIcon, - clusterdataplane: DnsIcon, - deploymentpipeline: AccountTreeIcon, - observabilityplane: VisibilityIcon, - clusterobservabilityplane: VisibilityIcon, - workflowplane: BuildIcon, - clusterworkflowplane: BuildIcon, - componenttype: CategoryIcon, - clustercomponenttype: CategoryIcon, - resourcetype: LayersIcon, - clusterresourcetype: LayersIcon, - resource: StorageIcon, - traittype: ExtensionIcon, - clustertraittype: ExtensionIcon, - workflow: PlayCircleOutlineIcon, - componentworkflow: SettingsApplicationsIcon, - }, - }), - }), + // DEFERRED to Step 3c: Custom EntityPresentationApi with kind icons for + // Environment, DataPlane, DeploymentPipeline, etc. Same conflict shape as + // catalogGraphApiRef above — the override needs to come from a catalog + // plugin module so it sits under pluginId `catalog`, not `app`. Until + // then, those kinds get the default upstream icon in the catalog graph. ]; diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index f6a4258dd..d1a79e00d 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -1,7 +1,7 @@ import '@backstage/cli/asset-types'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import app from './App'; import '@backstage/ui/css/styles.css'; import './buiOverrides.css'; -ReactDOM.createRoot(document.getElementById('root')!).render(); +ReactDOM.createRoot(document.getElementById('root')!).render(app); From 6d1ff413024a6ee1c85b34def48ecb13f5b949cb Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 14:20:21 +0530 Subject: [PATCH 08/44] feat(app): register custom NFS plugins and upstream scaffolder feature Step 3b of the NFS migration: - Wire the five custom plugins from Step 2 (openchoreo, openchoreo-ci, openchoreo-observability, openchoreo-workflows, platform-engineer-core) as NFS features via their `/alpha` default exports. - Drop the now-duplicate `openChoreoCiClientApiRef` and `genericWorkflowsClientApiRef` factory registrations from `apis.ts`; the plugins own them now. - Register `@backstage/plugin-scaffolder/alpha` so the legacy scaffolder route refs (used by `useRouteRef(scaffolderPlugin.routes.root)` inside `useKindCreateConfig`) resolve under NFS. Without it the catalog page crashes inside `ContextAwareCreateButton`. - Defer the scaffolder form-decorators override (custom `openChoreoTokenDecorator`) to Step 3c; under NFS the app-scoped factory collides with the scaffolder plugin's own default. Known regressions to fix in Step 3c: - `/create` shows upstream's NFS scaffolder Templates page, not our `CustomTemplateListPage`/`CustomReviewStep`/`ScaffolderLayout` or the 27 field extensions. They'll move to a scaffolder override + field- extension modules. - Catalog graph relations and entity-presentation kind icons remain on upstream defaults. Verified live: sign-in, home, catalog index, system entity page (all 9 observability tabs visible) render cleanly. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 27 +++++++++++++++++++- packages/app/src/apis.ts | 54 ++++++++-------------------------------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index d8b1b685b..45903ec83 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -73,6 +73,22 @@ import { convertLegacyAppRoot, } from '@backstage/core-compat-api'; import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; + +// NFS plugin features (created in Step 2 — each plugin's `/alpha` exports a +// `createFrontendPlugin` instance). These replace the API factory entries +// that previously lived in `apis.ts`. +import openchoreoPluginAlpha from '@openchoreo/backstage-plugin/alpha'; +import openchoreoCiPluginAlpha from '@openchoreo/backstage-plugin-openchoreo-ci/alpha'; +import openchoreoObservabilityPluginAlpha from '@openchoreo/backstage-plugin-openchoreo-observability/alpha'; +import openchoreoWorkflowsPluginAlpha from '@openchoreo/backstage-plugin-openchoreo-workflows/alpha'; +import platformEngineerCorePluginAlpha from '@openchoreo/backstage-plugin-platform-engineer-core/alpha'; + +// Upstream NFS plugin features. These register the route refs used by the +// legacy `bindRoutes` calls (scaffolder.routes.root, catalog.routes.catalogIndex, +// techdocs.routes.docRoot, catalogImport.routes.importPage). Their auto-mounted +// pages are deferred to Step 3c — for now the legacy `` mounts via +// `convertLegacyAppRoot` win. +import upstreamScaffolderPluginAlpha from '@backstage/plugin-scaffolder/alpha'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; @@ -289,7 +305,16 @@ const legacyRoot = convertLegacyAppRoot( ); const app = createApp({ - features: [legacyAppOptions, ...legacyRoot], + features: [ + legacyAppOptions, + upstreamScaffolderPluginAlpha, + openchoreoPluginAlpha, + openchoreoCiPluginAlpha, + openchoreoObservabilityPluginAlpha, + openchoreoWorkflowsPluginAlpha, + platformEngineerCorePluginAlpha, + ...legacyRoot, + ], bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index e1de52867..0651c5021 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -25,21 +25,8 @@ 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 { - formDecoratorsApiRef, - DefaultScaffolderFormDecoratorsApi, -} from '@backstage/plugin-scaffolder/alpha'; -import { openChoreoTokenDecorator } from './scaffolder/openChoreoTokenDecorator'; // Import from separate file to avoid circular dependency with form decorators import { openChoreoAuthApiRef } from './apis/authRefs'; -import { - openChoreoCiClientApiRef, - OpenChoreoCiClient, -} from '@openchoreo/backstage-plugin-openchoreo-ci'; -import { - genericWorkflowsClientApiRef, - GenericWorkflowsClient, -} from '@openchoreo/backstage-plugin-openchoreo-workflows'; // 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 @@ -140,27 +127,17 @@ export const apis: AnyApiFactory[] = [ factory: deps => UserSettingsStorage.create(deps), }), - // Form decorators for scaffolder - injects user's OpenChoreo token as a secret - // This enables user-based authorization in scaffolder actions - createApiFactory({ - api: formDecoratorsApiRef, - deps: {}, - factory: () => - DefaultScaffolderFormDecoratorsApi.create({ - decorators: [openChoreoTokenDecorator], - }), - }), + // DEFERRED to Step 3c: Scaffolder form decorators that inject the user's + // OpenChoreo token as a secret (used by scaffolder actions for user-based + // authorization). Under NFS this collides with the scaffolder plugin's + // own default factory (API_FACTORY_CONFLICT). Will reinstate via + // scaffolderPlugin.withOverrides so it lives under pluginId `scaffolder`. - // OpenChoreo CI client - provides API for workflow/build operations - createApiFactory({ - api: openChoreoCiClientApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new OpenChoreoCiClient(discoveryApi, fetchApi), - }), + // openChoreoCiClientApiRef and genericWorkflowsClientApiRef are now + // provided by their respective NFS plugins via `ApiBlueprint` (see + // plugins/openchoreo-ci/src/alpha.tsx and + // plugins/openchoreo-workflows/src/alpha.tsx). Registering them here + // would collide with the plugin-scoped factories under NFS. // DEFERRED to Step 3c: Catalog graph API override with custom OpenChoreo // relations. The legacy `app`-scoped registration of `catalogGraphApiRef` @@ -171,17 +148,6 @@ export const apis: AnyApiFactory[] = [ // custom relations (deploysTo, hostedOn, instanceOf, …) won't render in // entity Relations cards or the catalog graph. - // Generic Workflows client - provides API for org-level workflow operations - createApiFactory({ - api: genericWorkflowsClientApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new GenericWorkflowsClient(discoveryApi, fetchApi), - }), - // Assistant Agent client (Perch). Mirrors the registration on // openchoreoPerchPlugin.apis — see the import-site comment for why // both exist. From ff0d23b227c4a81733a070d552596e8ce778004b Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 14:26:34 +0530 Subject: [PATCH 09/44] feat(app): restore custom scaffolder UI under NFS Part of Step 3c (scaffolder customizations). Disable the upstream NFS \`page:scaffolder\` extension via \`scaffolderPlugin.withOverrides\` so the legacy \`\` mount in \`\` (preserved via \`convertLegacyAppRoot\`) owns the page again. The plugin's route refs and other extensions stay active so \`useRouteRef(scaffolderPlugin.routes.root)\` still resolves. \`convertLegacyAppRoot\` requires the route element to BE the routable extension; it cannot see through wrapper components. So: - Remove \`\` and \`\` from the \`/create\` route element. \`\` is now the direct element. - Hoist \`\` to wrap \`\` so the URL preselection still propagates to ProjectNamespaceField and NamespaceEntityPicker. The provider only reads \`useSearchParams\`, so wrapping the whole app is safe. - \`ScaffolderLayout\` (a CSS-only width constraint for scaffolder cards) is dropped for now; the cards will be unconstrained until a future CSS-scoped replacement lands in \`buiOverrides.css\`. Verified live: /create renders our \`CustomTemplateListPage\` (not upstream's Templates UI), template wizard shows custom field extensions and \`CustomReviewStep\`, catalog/home unchanged. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 52 +++++++++++++---------- packages/app/src/components/Root/Root.tsx | 3 ++ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 45903ec83..91ac4f803 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -41,8 +41,6 @@ import { DeploymentPipelineFormWithYamlFieldExtension } from './scaffolder/Deplo import { WorkloadDetailsFieldExtension } from './scaffolder/WorkloadDetailsField'; import { CustomTemplateListPage } from './components/scaffolder/CustomTemplateListPage'; import { CustomReviewStep } from './scaffolder/CustomReviewState'; -import { ScaffolderPreselectionProvider } from './scaffolder/ScaffolderPreselectionContext'; -import { ScaffolderLayout } from './scaffolder/ScaffolderLayout'; import { orgPlugin } from '@backstage/plugin-org'; import { SearchPage } from '@backstage/plugin-search'; import { @@ -83,12 +81,24 @@ import openchoreoObservabilityPluginAlpha from '@openchoreo/backstage-plugin-ope import openchoreoWorkflowsPluginAlpha from '@openchoreo/backstage-plugin-openchoreo-workflows/alpha'; import platformEngineerCorePluginAlpha from '@openchoreo/backstage-plugin-platform-engineer-core/alpha'; -// Upstream NFS plugin features. These register the route refs used by the -// legacy `bindRoutes` calls (scaffolder.routes.root, catalog.routes.catalogIndex, -// techdocs.routes.docRoot, catalogImport.routes.importPage). Their auto-mounted -// pages are deferred to Step 3c — for now the legacy `` mounts via -// `convertLegacyAppRoot` win. -import upstreamScaffolderPluginAlpha from '@backstage/plugin-scaffolder/alpha'; +// Upstream NFS plugin features. These register the route refs used by +// legacy `useRouteRef(scaffolderPlugin.routes.root)` calls (e.g. in +// useKindCreateConfig). We disable the upstream `scaffolderPage` extension +// because our legacy `` mount at `/create` in the FlatRoutes +// block — preserved via `convertLegacyAppRoot` — owns the page with all our +// customizations (CustomTemplateListPage, CustomReviewStep, ScaffolderLayout, +// 27 field extensions). +import upstreamScaffolderPluginAlphaBase from '@backstage/plugin-scaffolder/alpha'; + +const upstreamScaffolderPluginAlpha = upstreamScaffolderPluginAlphaBase.withOverrides( + { + extensions: [ + upstreamScaffolderPluginAlphaBase + .getExtension('page:scaffolder') + .override({ disabled: true }), + ], + }, +); import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; @@ -211,21 +221,17 @@ const routes = ( - - - - + } > diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index c08cbcc23..9b382d377 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -39,6 +39,7 @@ import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import CategoryIcon from '@material-ui/icons/Category'; import BubbleChartIcon from '@material-ui/icons/BubbleChart'; import { AssistantDrawerProvider } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; +import { ScaffolderPreselectionProvider } from '../../scaffolder/ScaffolderPreselectionContext'; const isMac = typeof navigator !== 'undefined' && @@ -168,6 +169,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { useSearchModalStyles(); const a11yClasses = useA11yStyles(); return ( + Skip to main content @@ -238,5 +240,6 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { + ); }; From 51da9b1ea0b47ddb13793c5e8d04a1e8763d8157 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 14:30:59 +0530 Subject: [PATCH 10/44] feat(app): restore catalog/scaffolder API overrides via plugin withOverrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of Step 3c (continued). Move the three upstream API factories that we customize under their owning plugin's pluginId, replacing the `app`-scoped registrations that previously caused API_FACTORY_CONFLICT under NFS: - catalog-graph: \`api:catalog-graph\` override carries the OpenChoreo custom relations (deploysTo, hostedOn, instanceOf, …) and relation pairs. - catalog: \`api:catalog/entity-presentation\` override carries the kind icons for Environment, DataPlane, DeploymentPipeline, etc. - scaffolder: \`api:scaffolder/form-decorators\` override re-injects the \`openChoreoTokenDecorator\` for user-based authorization on scaffolder actions. The \`page:scaffolder\` extension disable from the previous commit moves into the same overrides module. The override pattern uses \`pluginAlpha.getExtension(id).override({ params: defineParams => defineParams({...}) })\` since ApiBlueprint takes its params as a callback in the v0.17 frontend-plugin-api. All three overrides live in a new \`packages/app/src/apis/customOverrides.tsx\` to keep App.tsx tidy. Verified live: catalog index, catalog graph, system entity Relations card, scaffolder template wizard all render with our customizations active and no startup conflicts. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 30 ++-- packages/app/src/apis/customOverrides.tsx | 175 ++++++++++++++++++++++ 2 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 packages/app/src/apis/customOverrides.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 91ac4f803..6167614ee 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -81,24 +81,16 @@ import openchoreoObservabilityPluginAlpha from '@openchoreo/backstage-plugin-ope import openchoreoWorkflowsPluginAlpha from '@openchoreo/backstage-plugin-openchoreo-workflows/alpha'; import platformEngineerCorePluginAlpha from '@openchoreo/backstage-plugin-platform-engineer-core/alpha'; -// Upstream NFS plugin features. These register the route refs used by -// legacy `useRouteRef(scaffolderPlugin.routes.root)` calls (e.g. in -// useKindCreateConfig). We disable the upstream `scaffolderPage` extension -// because our legacy `` mount at `/create` in the FlatRoutes -// block — preserved via `convertLegacyAppRoot` — owns the page with all our -// customizations (CustomTemplateListPage, CustomReviewStep, ScaffolderLayout, -// 27 field extensions). -import upstreamScaffolderPluginAlphaBase from '@backstage/plugin-scaffolder/alpha'; - -const upstreamScaffolderPluginAlpha = upstreamScaffolderPluginAlphaBase.withOverrides( - { - extensions: [ - upstreamScaffolderPluginAlphaBase - .getExtension('page:scaffolder') - .override({ disabled: true }), - ], - }, -); +// Upstream NFS plugin features with our overrides: +// - catalog graph default API replaced to include OpenChoreo custom relations +// - catalog entity-presentation default API replaced to add custom kind icons +// - scaffolder `page:scaffolder` disabled (our legacy wins) +// and form-decorators API replaced to inject the openChoreoTokenDecorator +import { + catalogGraphPluginAlpha, + catalogPluginAlpha, + scaffolderPluginAlpha as upstreamScaffolderPluginAlpha, +} from './apis/customOverrides'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; @@ -314,6 +306,8 @@ const app = createApp({ features: [ legacyAppOptions, upstreamScaffolderPluginAlpha, + catalogGraphPluginAlpha, + catalogPluginAlpha, openchoreoPluginAlpha, openchoreoCiPluginAlpha, openchoreoObservabilityPluginAlpha, diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx new file mode 100644 index 000000000..a4b916622 --- /dev/null +++ b/packages/app/src/apis/customOverrides.tsx @@ -0,0 +1,175 @@ +/** + * Step 3c — plugin-scoped overrides for upstream NFS plugin APIs we customize. + * + * Under NFS, registering a custom API factory under our `app` plugin id + * collides with the upstream plugin that already owns that API id + * (API_FACTORY_CONFLICT). Instead, override the existing extension under the + * upstream plugin's own pluginId via `withOverrides({ extensions: [...] })`. + */ + +import catalogGraphPluginAlphaBase from '@backstage/plugin-catalog-graph/alpha'; +import catalogPluginAlphaBase from '@backstage/plugin-catalog/alpha'; +import scaffolderPluginAlphaBase from '@backstage/plugin-scaffolder/alpha'; +import { + catalogGraphApiRef, + DefaultCatalogGraphApi, + ALL_RELATIONS, + ALL_RELATION_PAIRS, +} from '@backstage/plugin-catalog-graph'; +import { + catalogApiRef, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; +import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; +import { + formDecoratorsApiRef, + DefaultScaffolderFormDecoratorsApi, +} from '@backstage/plugin-scaffolder/alpha'; +import { + RELATION_DEPLOYS_TO, + RELATION_DEPLOYED_BY, + RELATION_USES_PIPELINE, + RELATION_PIPELINE_USED_BY, + RELATION_HOSTED_ON, + RELATION_HOSTS, + RELATION_OBSERVED_BY, + RELATION_OBSERVES, + RELATION_INSTANCE_OF, + RELATION_HAS_INSTANCE, + RELATION_USES_WORKFLOW, + RELATION_WORKFLOW_USED_BY, + RELATION_BUILDS_ON, + RELATION_BUILDS, +} from '@openchoreo/backstage-plugin-common'; +import CloudIcon from '@material-ui/icons/Cloud'; +import DnsIcon from '@material-ui/icons/Dns'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import BuildIcon from '@material-ui/icons/Build'; +import CategoryIcon from '@material-ui/icons/Category'; +import LayersIcon from '@material-ui/icons/Layers'; +import StorageIcon from '@material-ui/icons/Storage'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; +import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; +import { openChoreoTokenDecorator } from '../scaffolder/openChoreoTokenDecorator'; + +/** + * Override `catalog-graph`'s default `api:catalog-graph` to include the + * custom OpenChoreo relations (deploysTo, hostedOn, instanceOf, …). Without + * this, custom relations don't render in entity Relations cards or the + * catalog graph. + */ +export const catalogGraphPluginAlpha = catalogGraphPluginAlphaBase.withOverrides( + { + extensions: [ + catalogGraphPluginAlphaBase.getExtension('api:catalog-graph').override({ + params: defineParams => + defineParams({ + api: catalogGraphApiRef, + deps: {}, + factory: () => + new DefaultCatalogGraphApi({ + knownRelations: [ + ...ALL_RELATIONS, + RELATION_DEPLOYS_TO, + RELATION_DEPLOYED_BY, + RELATION_USES_PIPELINE, + RELATION_PIPELINE_USED_BY, + RELATION_HOSTED_ON, + RELATION_HOSTS, + RELATION_OBSERVED_BY, + RELATION_OBSERVES, + RELATION_INSTANCE_OF, + RELATION_HAS_INSTANCE, + RELATION_USES_WORKFLOW, + RELATION_WORKFLOW_USED_BY, + RELATION_BUILDS_ON, + RELATION_BUILDS, + ], + knownRelationPairs: [ + ...ALL_RELATION_PAIRS, + [RELATION_DEPLOYS_TO, RELATION_DEPLOYED_BY], + [RELATION_USES_PIPELINE, RELATION_PIPELINE_USED_BY], + [RELATION_HOSTED_ON, RELATION_HOSTS], + [RELATION_OBSERVED_BY, RELATION_OBSERVES], + [RELATION_INSTANCE_OF, RELATION_HAS_INSTANCE], + [RELATION_USES_WORKFLOW, RELATION_WORKFLOW_USED_BY], + [RELATION_BUILDS_ON, RELATION_BUILDS], + ], + defaultRelationTypes: { exclude: [] }, + }), + }), + }), + ], + }, +); + +/** + * Override `catalog`'s default `api:catalog/entity-presentation` to provide + * kind icons for OpenChoreo-specific entity kinds (Environment, DataPlane, + * DeploymentPipeline, etc.) in the catalog graph and entity views. + */ +export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ + extensions: [ + catalogPluginAlphaBase + .getExtension('api:catalog/entity-presentation') + .override({ + params: defineParams => + defineParams({ + api: entityPresentationApiRef, + deps: { catalogApi: catalogApiRef }, + factory: ({ catalogApi }) => + DefaultEntityPresentationApi.create({ + catalogApi, + kindIcons: { + environment: CloudIcon, + dataplane: DnsIcon, + clusterdataplane: DnsIcon, + deploymentpipeline: AccountTreeIcon, + observabilityplane: VisibilityIcon, + clusterobservabilityplane: VisibilityIcon, + workflowplane: BuildIcon, + clusterworkflowplane: BuildIcon, + componenttype: CategoryIcon, + clustercomponenttype: CategoryIcon, + resourcetype: LayersIcon, + clusterresourcetype: LayersIcon, + resource: StorageIcon, + traittype: ExtensionIcon, + clustertraittype: ExtensionIcon, + workflow: PlayCircleOutlineIcon, + componentworkflow: SettingsApplicationsIcon, + }, + }), + }), + }), + ], +}); + +/** + * Override `scaffolder`'s default `page:scaffolder` (disabled — the legacy + * `` mount at `/create` wins) and + * `api:scaffolder/form-decorators` to inject the user's OpenChoreo token as + * a secret for user-based authorization in scaffolder actions. + */ +export const scaffolderPluginAlpha = scaffolderPluginAlphaBase.withOverrides({ + extensions: [ + scaffolderPluginAlphaBase + .getExtension('page:scaffolder') + .override({ disabled: true }), + scaffolderPluginAlphaBase + .getExtension('api:scaffolder/form-decorators') + .override({ + params: defineParams => + defineParams({ + api: formDecoratorsApiRef, + deps: {}, + factory: () => + DefaultScaffolderFormDecoratorsApi.create({ + decorators: [openChoreoTokenDecorator], + }), + }), + }), + ], +}); From 84deb4bbf2f9d16673df0c9496c0da46ad6ef12f Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 14:33:47 +0530 Subject: [PATCH 11/44] feat(app): restore catalog-import translations and register its NFS plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final piece of Step 3c. Add a TranslationBlueprint module that reinstates the catalog-import header overrides (`Register an existing catalog entity` etc.) that previously rode via createApp.__experimentalTranslations. Register \`@backstage/plugin-catalog-import/alpha\` so the \`routeRef{id=catalog-import}\` resolves under NFS; without it the legacy \`\` mount inside fails at runtime with "Routable extension component … was not discovered". Verified live: /catalog-import shows the customized title and supporting text, no errors. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 15 ++++++---- packages/app/src/apis/customOverrides.tsx | 36 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 6167614ee..2aec09956 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -86,11 +86,19 @@ import platformEngineerCorePluginAlpha from '@openchoreo/backstage-plugin-platfo // - catalog entity-presentation default API replaced to add custom kind icons // - scaffolder `page:scaffolder` disabled (our legacy wins) // and form-decorators API replaced to inject the openChoreoTokenDecorator +// - customTranslationsModule reinstates the catalog-import header overrides +// that previously rode via createApp.__experimentalTranslations import { catalogGraphPluginAlpha, catalogPluginAlpha, + customTranslationsModule, scaffolderPluginAlpha as upstreamScaffolderPluginAlpha, } from './apis/customOverrides'; + +// catalog-import NFS plugin — registered so the `/catalog-import` route ref +// resolves under NFS. Our legacy `` +// mount in `` provides the actual page rendering. +import catalogImportPluginAlpha from '@backstage/plugin-catalog-import/alpha'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; @@ -156,11 +164,6 @@ function DynamicSignInPage(props: any) { ); } -// NOTE: catalog-import translation overrides (previously passed via -// `__experimentalTranslations`) are deferred to Step 3c — they'll move into a -// `TranslationBlueprint.make({...})` module. Until then, catalog-import shows -// upstream's default header strings. - const legacyAppOptions = convertLegacyAppOptions({ apis, icons: { @@ -305,9 +308,11 @@ const legacyRoot = convertLegacyAppRoot( const app = createApp({ features: [ legacyAppOptions, + customTranslationsModule, upstreamScaffolderPluginAlpha, catalogGraphPluginAlpha, catalogPluginAlpha, + catalogImportPluginAlpha, openchoreoPluginAlpha, openchoreoCiPluginAlpha, openchoreoObservabilityPluginAlpha, diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index a4b916622..22a46c8b7 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -10,6 +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 { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; +import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; import { catalogGraphApiRef, DefaultCatalogGraphApi, @@ -153,6 +157,38 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ * `api:scaffolder/form-decorators` to inject the user's OpenChoreo token as * a secret for user-based authorization in scaffolder actions. */ +/** + * App-scoped translation overrides that previously rode in via + * `createApp.__experimentalTranslations`. The catalog-import header strings + * are customized to read "Register an existing catalog entity" rather than + * the upstream default "Register Software". + */ +export const customTranslationsModule = createFrontendModule({ + pluginId: 'app', + extensions: [ + TranslationBlueprint.make({ + name: 'catalog-import-overrides', + params: { + resource: createTranslationMessages({ + ref: catalogImportTranslationRef, + full: false, + messages: { + 'defaultImportPage.headerTitle': 'Register an existing catalog entity', + 'defaultImportPage.contentHeaderTitle': + 'Start tracking your entity in {{appTitle}}', + 'defaultImportPage.supportTitle': + 'Start tracking your entity in {{appTitle}} by adding it to the software catalog.', + 'importInfoCard.title': 'Register an existing catalog entity', + 'stepInitAnalyzeUrl.urlHelperText': + 'Enter the full path to your entity file to start tracking', + 'stepFinishImportLocation.locations.viewButtonText': 'View Entity', + }, + }), + }, + }), + ], +}); + export const scaffolderPluginAlpha = scaffolderPluginAlphaBase.withOverrides({ extensions: [ scaffolderPluginAlphaBase From cbfe5bc3e8fbfb8ff501012f34660c5324d86ba1 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 14:48:52 +0530 Subject: [PATCH 12/44] feat(app): migrate DynamicSignInPage to SignInPageBlueprint extension Step 3d. Move \`DynamicSignInPage\` (the OIDC vs guest auth switcher) out of App.tsx into its own \`packages/app/src/components/DynamicSignInPage.tsx\` and register it through \`SignInPageBlueprint.make({ loader: () => import(...) })\` inside the renamed \`customAppModule\` (was \`customTranslationsModule\`, now holds both the sign-in extension and the translation overrides since both target \`pluginId: 'app'\`). Drop \`components.SignInPage: DynamicSignInPage\` from \`convertLegacyAppOptions\` and the now-unused \`SignInPage\`/\`configApiRef\`/\`useApi\`/\`openChoreoAuthApiRef\` imports from App.tsx. Verified live: signed out, the OpenChoreo \"Sign in using OpenChoreo\" button appears via the NFS blueprint, OIDC redirect to Thunder works, sign-in completes, home page renders. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 48 ++----------------- packages/app/src/apis/customOverrides.tsx | 25 +++++++--- .../app/src/components/DynamicSignInPage.tsx | 41 ++++++++++++++++ 3 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 packages/app/src/components/DynamicSignInPage.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 2aec09956..17fcac2cc 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -50,7 +50,7 @@ import { } from '@backstage/plugin-techdocs'; import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; -import { apis, openChoreoAuthApiRef } from './apis'; +import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; import { CustomCatalogPage } from './components/catalog/CustomCatalogPage'; import { CustomApiExplorerPage } from './components/catalog/CustomApiExplorerPage'; @@ -63,7 +63,6 @@ import { PlatformOverviewPage } from './components/platformOverview'; import { AlertDisplay, OAuthRequestDialog, - SignInPage, } from '@backstage/core-components'; import { createApp } from '@backstage/frontend-defaults'; import { @@ -91,7 +90,7 @@ import platformEngineerCorePluginAlpha from '@openchoreo/backstage-plugin-platfo import { catalogGraphPluginAlpha, catalogPluginAlpha, - customTranslationsModule, + customAppModule, scaffolderPluginAlpha as upstreamScaffolderPluginAlpha, } from './apis/customOverrides'; @@ -124,46 +123,8 @@ import ExtensionIcon from '@material-ui/icons/Extension'; import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; import { VisitListener } from '@backstage/plugin-home'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { DependencyGraphZoomOverrides } from './components/graph/DependencyGraphZoomOverrides'; -/** - * Dynamic SignInPage that switches between OAuth and Guest mode - * based on openchoreo.features.auth.enabled configuration. - * - * When auth is enabled (default): Uses OpenChoreo IDP OAuth flow - * When auth is disabled: Auto-signs in as guest user using Backstage's built-in guest provider - */ -function DynamicSignInPage(props: any) { - const configApi = useApi(configApiRef); - - // Check if auth feature is enabled (defaults to true) - const authEnabled = - configApi.getOptionalBoolean('openchoreo.features.auth.enabled') ?? true; - - if (!authEnabled) { - // Guest mode: use Backstage's built-in guest provider - // This uses ProxiedSignInIdentity with the backend guest module - // and falls back to GuestUserIdentity (legacy) if not available - return ; - } - - // Default: OpenChoreo Auth (works with any OIDC-compliant IDP). - // The sign-in page is always shown with a login button. The OAuth2 provider - // handles popup vs. redirect based on the enableExperimentalRedirectFlow config. - return ( - - ); -} - const legacyAppOptions = convertLegacyAppOptions({ apis, icons: { @@ -186,9 +147,6 @@ const legacyAppOptions = convertLegacyAppOptions({ 'kind:clusterworkflow': PlayCircleOutlineIcon, 'kind:componentworkflow': SettingsApplicationsIcon, }, - components: { - SignInPage: DynamicSignInPage, - }, themes: appThemes, }); @@ -308,7 +266,7 @@ const legacyRoot = convertLegacyAppRoot( const app = createApp({ features: [ legacyAppOptions, - customTranslationsModule, + customAppModule, upstreamScaffolderPluginAlpha, catalogGraphPluginAlpha, catalogPluginAlpha, diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index 22a46c8b7..4de2b7888 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -12,7 +12,10 @@ import catalogPluginAlphaBase from '@backstage/plugin-catalog/alpha'; import scaffolderPluginAlphaBase from '@backstage/plugin-scaffolder/alpha'; import { createFrontendModule } from '@backstage/frontend-plugin-api'; import { createTranslationMessages } from '@backstage/frontend-plugin-api'; -import { TranslationBlueprint } from '@backstage/plugin-app-react'; +import { + SignInPageBlueprint, + TranslationBlueprint, +} from '@backstage/plugin-app-react'; import { catalogImportTranslationRef } from '@backstage/plugin-catalog-import/alpha'; import { catalogGraphApiRef, @@ -158,14 +161,24 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ * a secret for user-based authorization in scaffolder actions. */ /** - * App-scoped translation overrides that previously rode in via - * `createApp.__experimentalTranslations`. The catalog-import header strings - * are customized to read "Register an existing catalog entity" rather than - * the upstream default "Register Software". + * App-scoped extensions: + * - SignInPage: lazy-loaded DynamicSignInPage that switches between + * OpenChoreo OIDC and guest mode based on `openchoreo.features.auth.enabled`. + * Replaces the legacy `createApp.components.SignInPage` slot. + * - Translation overrides for catalog-import that previously rode via + * `createApp.__experimentalTranslations`. Customizes the header strings to + * read "Register an existing catalog entity" rather than the upstream + * default "Register Software". */ -export const customTranslationsModule = createFrontendModule({ +export const customAppModule = createFrontendModule({ pluginId: 'app', extensions: [ + SignInPageBlueprint.make({ + params: { + loader: () => + import('../components/DynamicSignInPage').then(m => m.default), + }, + }), TranslationBlueprint.make({ name: 'catalog-import-overrides', params: { diff --git a/packages/app/src/components/DynamicSignInPage.tsx b/packages/app/src/components/DynamicSignInPage.tsx new file mode 100644 index 000000000..b74a97045 --- /dev/null +++ b/packages/app/src/components/DynamicSignInPage.tsx @@ -0,0 +1,41 @@ +import { SignInPage } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import type { SignInPageProps } from '@backstage/plugin-app-react'; +import { openChoreoAuthApiRef } from '../apis'; + +/** + * Dynamic SignInPage that switches between OpenChoreo OIDC and guest mode + * based on `openchoreo.features.auth.enabled`. + * + * - `auth.enabled = true` (default): OpenChoreo IDP OAuth via the + * `openChoreoAuthApiRef`. The SignInPage shows a login button. + * - `auth.enabled = false`: Backstage's built-in `guest` provider with + * `auto`, so we auto-sign-in. + * + * Mounted as an NFS `SignInPageBlueprint` extension (see + * `apis/customOverrides.tsx`), replacing the legacy `createApp.components. + * SignInPage` slot. + */ +export function DynamicSignInPage(props: SignInPageProps) { + const configApi = useApi(configApiRef); + const authEnabled = + configApi.getOptionalBoolean('openchoreo.features.auth.enabled') ?? true; + + if (!authEnabled) { + return ; + } + + return ( + + ); +} + +export default DynamicSignInPage; From 872f013739d250a572cf0ce2e1138996fc489202 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 15:07:41 +0530 Subject: [PATCH 13/44] chore: add changeset for NFS migration Patch bump for the five custom plugins that gained `/alpha` NFS entry points (openchoreo, openchoreo-ci, openchoreo-observability, openchoreo-workflows, platform-engineer-core). Default export remains the legacy `createPlugin` instance; new entry exposes the same plugin as a `createFrontendPlugin` for use with `createApp({ features })`. Signed-off-by: Kavith Lokuhewage --- .changeset/migrate-portal-to-nfs.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/migrate-portal-to-nfs.md diff --git a/.changeset/migrate-portal-to-nfs.md b/.changeset/migrate-portal-to-nfs.md new file mode 100644 index 000000000..effe99259 --- /dev/null +++ b/.changeset/migrate-portal-to-nfs.md @@ -0,0 +1,13 @@ +--- +'@openchoreo/backstage-plugin': patch +'@openchoreo/backstage-plugin-openchoreo-ci': patch +'@openchoreo/backstage-plugin-openchoreo-observability': patch +'@openchoreo/backstage-plugin-openchoreo-workflows': patch +'@openchoreo/backstage-plugin-platform-engineer-core': patch +--- + +Add an `/alpha` entry point that exposes each plugin as a `createFrontendPlugin` for use with Backstage's New Frontend System (NFS). The default entry continues to export the legacy `createPlugin` instance so existing host apps keep working unchanged; adopters on NFS can now import `from '@openchoreo/backstage-plugin-/alpha'` and include the plugin directly in `createApp({ features: [...] })`. + +The `/alpha` exports register each plugin's API factories (e.g. `openChoreoCiClientApiRef`, `genericWorkflowsClientApiRef`, the three observability backend clients, `openChoreoClientApiRef`) and one top-level page where applicable (`platform-engineer-core`'s dashboard view, `openchoreo-workflows`' generic workflows page, `openchoreo-ci`'s workflows entity tab). Entity tabs and component cards that the host app mounts with per-call props (e.g. ``) remain on the legacy export for now; a future release will move those host-injected callables behind a registry API so they can ride through NFS extensions too. + +This addresses the body of [openchoreo/openchoreo#3568](https://github.com/openchoreo/openchoreo/issues/3568) — adopters can drop `--legacy` from the `@backstage/create-app` step when installing the plugin suite into an existing Backstage host. From 4b1330354d727021f64e60f716d857392705a5b0 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 15:42:42 +0530 Subject: [PATCH 14/44] fix: formatting Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 5 +- packages/app/src/apis/customOverrides.tsx | 10 +- packages/app/src/components/Root/Root.tsx | 138 +++++++++++----------- 3 files changed, 75 insertions(+), 78 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 17fcac2cc..4f041c1fa 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -60,10 +60,7 @@ import { HomePage } from './components/Home'; import { CustomGraphNode } from '@openchoreo/backstage-plugin-react'; import { PlatformOverviewPage } from './components/platformOverview'; -import { - AlertDisplay, - OAuthRequestDialog, -} from '@backstage/core-components'; +import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; import { createApp } from '@backstage/frontend-defaults'; import { convertLegacyAppOptions, diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index 4de2b7888..ae12e51cb 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -67,8 +67,8 @@ import { openChoreoTokenDecorator } from '../scaffolder/openChoreoTokenDecorator * this, custom relations don't render in entity Relations cards or the * catalog graph. */ -export const catalogGraphPluginAlpha = catalogGraphPluginAlphaBase.withOverrides( - { +export const catalogGraphPluginAlpha = + catalogGraphPluginAlphaBase.withOverrides({ extensions: [ catalogGraphPluginAlphaBase.getExtension('api:catalog-graph').override({ params: defineParams => @@ -109,8 +109,7 @@ export const catalogGraphPluginAlpha = catalogGraphPluginAlphaBase.withOverrides }), }), ], - }, -); + }); /** * Override `catalog`'s default `api:catalog/entity-presentation` to provide @@ -186,7 +185,8 @@ export const customAppModule = createFrontendModule({ ref: catalogImportTranslationRef, full: false, messages: { - 'defaultImportPage.headerTitle': 'Register an existing catalog entity', + 'defaultImportPage.headerTitle': + 'Register an existing catalog entity', 'defaultImportPage.contentHeaderTitle': 'Start tracking your entity in {{appTitle}}', 'defaultImportPage.supportTitle': diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 9b382d377..ae57a84d8 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -170,76 +170,76 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { const a11yClasses = useA11yStyles(); return ( - - - Skip to main content - - - - - -
- } to="/search"> - - - - {({ toggleModal }) => ( - - )} - - - -
-
- - }> - {/* Global nav, not org-specific */} - - - - - - {/* TechDocs disabled until proper production support is implemented */} - {/* */} - - {/* End global nav */} - - {/* Items in this group will be scrollable if they run out of space */} - - - - - } - to="/settings" + + + Skip to main content + + + + + +
+ } to="/search"> + + + + {({ toggleModal }) => ( + + )} + + + +
+
+ + }> + {/* Global nav, not org-specific */} + + + + + + {/* TechDocs disabled until proper production support is implemented */} + {/* */} + + {/* End global nav */} + + {/* Items in this group will be scrollable if they run out of space */} + + + + + } + to="/settings" + > + + + + +
+
- - - - - -
- {children} -
- - + {children} +
+
+
); }; From f7c76902de786eff4a85fe3cc1b63392e9d897c8 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 9 Jun 2026 15:45:10 +0530 Subject: [PATCH 15/44] fix: missing clusterworkflow in kindIcons Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis/customOverrides.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index ae12e51cb..ac1de1f83 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -145,6 +145,7 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ traittype: ExtensionIcon, clustertraittype: ExtensionIcon, workflow: PlayCircleOutlineIcon, + clusterworkflow: PlayCircleOutlineIcon, componentworkflow: SettingsApplicationsIcon, }, }), From cd7128264e477cdcd147dfcae0a5ad43f23a8ff1 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 10 Jun 2026 04:58:35 +0530 Subject: [PATCH 16/44] test(app): drop stale apis registry assertions for NFS-migrated factories The CI/workflows clients and catalog-graph factory moved out of app-scope apis.ts into plugin /alpha modules and customOverrides.tsx during the NFS migration; this test was still asserting them in the app-scoped apis array. Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis.test.ts | 41 +++++------------------------------ 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/packages/app/src/apis.test.ts b/packages/app/src/apis.test.ts index 480200d82..f1d69f192 100644 --- a/packages/app/src/apis.test.ts +++ b/packages/app/src/apis.test.ts @@ -9,6 +9,12 @@ * * 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. */ import { AnyApiFactory, @@ -18,15 +24,6 @@ import { import { permissionApiRef } from '@backstage/plugin-permission-react'; import { visitsApiRef } from '@backstage/plugin-home'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { catalogGraphApiRef } from '@backstage/plugin-catalog-graph'; -import { - openChoreoCiClientApiRef, - OpenChoreoCiClient, -} from '@openchoreo/backstage-plugin-openchoreo-ci'; -import { - genericWorkflowsClientApiRef, - GenericWorkflowsClient, -} from '@openchoreo/backstage-plugin-openchoreo-workflows'; import { perchAgentApiRef, PerchAgentClient, @@ -72,32 +69,12 @@ describe('apis registry', () => { openChoreoAuthApiRef, visitsApiRef, storageApiRef, - openChoreoCiClientApiRef, - genericWorkflowsClientApiRef, perchAgentApiRef, ]) { expect(ids).toContain(ref.id); } }); - it('builds the OpenChoreoCiClient via its factory', () => { - const f = findFactory(apis, openChoreoCiClientApiRef); - const instance = invoke(f, { - discoveryApi: stubDiscovery, - fetchApi: stubFetch, - }); - expect(instance).toBeInstanceOf(OpenChoreoCiClient); - }); - - it('builds the GenericWorkflowsClient via its factory', () => { - const f = findFactory(apis, genericWorkflowsClientApiRef); - const instance = invoke(f, { - discoveryApi: stubDiscovery, - fetchApi: stubFetch, - }); - expect(instance).toBeInstanceOf(GenericWorkflowsClient); - }); - it('builds the PerchAgentClient via its factory', () => { const f = findFactory(apis, perchAgentApiRef); const instance = invoke(f, { @@ -115,10 +92,4 @@ describe('apis registry', () => { }); expect(instance).toBeDefined(); }); - - it('builds the catalog graph api with custom OpenChoreo relations', () => { - const f = findFactory(apis, catalogGraphApiRef); - const instance = invoke(f, {}); - expect(instance).toBeDefined(); - }); }); From 51947bf9e3f2394e69578d38bcc73827df821e10 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:00:41 +0530 Subject: [PATCH 17/44] feat(observability): expose log-row-action registry API for NFS host injection Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis/customOverrides.tsx | 16 ++++++ .../openchoreo-observability/src/alpha.tsx | 57 ++++++++++++++++--- .../src/alpha/LogRowActionBlueprint.ts | 35 ++++++++++++ .../src/api/LogRowActionRendererApi.ts | 40 +++++++++++++ .../openchoreo-observability/src/api/index.ts | 6 ++ .../ObservabilityProjectRuntimeLogsPage.tsx | 14 ++++- .../ObservabilityRuntimeLogsPage.tsx | 16 +++++- plugins/openchoreo-observability/src/index.ts | 4 ++ 8 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 plugins/openchoreo-observability/src/alpha/LogRowActionBlueprint.ts create mode 100644 plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index ac1de1f83..d7ac7969c 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -60,6 +60,8 @@ import ExtensionIcon from '@material-ui/icons/Extension'; import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; import { openChoreoTokenDecorator } from '../scaffolder/openChoreoTokenDecorator'; +import { LogRowActionBlueprint } from '@openchoreo/backstage-plugin-openchoreo-observability/alpha'; +import { InvestigateLogButton } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; /** * Override `catalog-graph`'s default `api:catalog-graph` to include the @@ -200,6 +202,20 @@ export const customAppModule = createFrontendModule({ }), }, }), + // Host-injected per-row action renderer for the observability + // runtime-logs tables. Wires the portal-assistant's + // InvestigateLogButton into ObservabilityRuntimeLogs / + // ObservabilityProjectRuntimeLogs without coupling the + // observability plugin to portal-assistant. Mirrors upstream's + // FormDecoratorBlueprint registration pattern. + LogRowActionBlueprint.make({ + name: 'investigate-log', + params: { + renderer: (log, getLogsSnapshot) => ( + + ), + }, + }), ], }); diff --git a/plugins/openchoreo-observability/src/alpha.tsx b/plugins/openchoreo-observability/src/alpha.tsx index a1094d62a..45a157c97 100644 --- a/plugins/openchoreo-observability/src/alpha.tsx +++ b/plugins/openchoreo-observability/src/alpha.tsx @@ -1,5 +1,6 @@ import { ApiBlueprint, + createExtensionInput, createFrontendPlugin, discoveryApiRef, fetchApiRef, @@ -12,6 +13,17 @@ import { } from './api/ObservabilityApi'; import { rcaAgentApiRef, RCAAgentClient } from './api/RCAAgentApi'; import { finopsAgentApiRef, FinOpsAgentClient } from './api/FinOpsAgentApi'; +import { + DefaultLogRowActionRendererApi, + logRowActionRendererApiRef, +} from './api/LogRowActionRendererApi'; +import { LogRowActionBlueprint } from './alpha/LogRowActionBlueprint'; + +export { LogRowActionBlueprint } from './alpha/LogRowActionBlueprint'; +export { + logRowActionRendererApiRef, + type LogRowActionRendererApi, +} from './api/LogRowActionRendererApi'; const observabilityApi = ApiBlueprint.make({ name: 'observability', @@ -46,19 +58,48 @@ const finopsAgentApi = ApiBlueprint.make({ }), }); +/** + * Registry API for host-injected log-row action renderers. Collects every + * `LogRowActionBlueprint` extension contributed by the host (or any other + * plugin) and exposes the first renderer via `useApi(logRowActionRendererApiRef)`. + * + * Mirrors upstream's `formDecoratorsApi` (plugin-scaffolder) — see + * `node_modules/@backstage/plugin-scaffolder/dist/alpha/api/FormDecoratorsApi.esm.js`. + */ +const logRowActionRendererApi = ApiBlueprint.makeWithOverrides({ + name: 'log-row-action-renderer', + inputs: { + renderers: createExtensionInput([LogRowActionBlueprint.dataRefs.renderer]), + }, + factory(originalFactory, { inputs }) { + const renderers = inputs.renderers.map(e => + e.get(LogRowActionBlueprint.dataRefs.renderer), + ); + return originalFactory(defineParams => + defineParams({ + api: logRowActionRendererApiRef, + deps: {}, + factory: () => DefaultLogRowActionRendererApi.create({ renderers }), + }), + ); + }, +}); + /** * NFS entry point for the OpenChoreo Observability plugin. * - * Registers the three observability backend clients. The legacy - * `createRoutableExtension` page components (ObservabilityMetrics, - * ObservabilityTraces, …) continue to flow through `src/plugin.ts` and are - * mounted manually by the host app via `` blocks so they - * can receive per-mount props (e.g. `renderRowAction`). When the host moves - * those mounts to NFS-driven entity tabs, additional EntityContentBlueprint - * extensions can be added here. + * Registers the three observability backend clients plus the + * log-row-action registry API. Entity tabs (Metrics, Traces, RCA, + * RuntimeLogs, etc.) ride through the legacy `src/plugin.ts` until + * a follow-up commit converts them to `EntityContentBlueprint`. */ export default createFrontendPlugin({ pluginId: 'openchoreo-observability', routes: { root: rootRouteRef }, - extensions: [observabilityApi, rcaAgentApi, finopsAgentApi], + extensions: [ + observabilityApi, + rcaAgentApi, + finopsAgentApi, + logRowActionRendererApi, + ], }); diff --git a/plugins/openchoreo-observability/src/alpha/LogRowActionBlueprint.ts b/plugins/openchoreo-observability/src/alpha/LogRowActionBlueprint.ts new file mode 100644 index 000000000..dbbe1665b --- /dev/null +++ b/plugins/openchoreo-observability/src/alpha/LogRowActionBlueprint.ts @@ -0,0 +1,35 @@ +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; +import type { RenderLogRowAction } from '../components/RuntimeLogs/LogEntry'; + +const logRowActionRendererExtensionDataRef = + createExtensionDataRef().with({ + id: 'openchoreo-observability.log-row-action-renderer', + }); + +/** + * NFS extension blueprint that hosts use to contribute a per-row action + * renderer to the observability runtime-logs tables. + * + * Mirrors upstream's `FormDecoratorBlueprint` pattern. Each blueprint + * extension `attachTo`s the `renderers` input of + * `api:openchoreo-observability/log-row-action-renderer`; the API's + * factory then collects them into a single renderer that the logs + * tables consume via `useApi(logRowActionRendererApiRef)`. + */ +export const LogRowActionBlueprint = createExtensionBlueprint({ + kind: 'log-row-action-renderer', + attachTo: { + id: 'api:openchoreo-observability/log-row-action-renderer', + input: 'renderers', + }, + dataRefs: { + renderer: logRowActionRendererExtensionDataRef, + }, + output: [logRowActionRendererExtensionDataRef], + *factory(params: { renderer: RenderLogRowAction }) { + yield logRowActionRendererExtensionDataRef(params.renderer); + }, +}); diff --git a/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts b/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts new file mode 100644 index 000000000..4b6c9c496 --- /dev/null +++ b/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts @@ -0,0 +1,40 @@ +import { createApiRef } from '@backstage/core-plugin-api'; +import type { RenderLogRowAction } from '../components/RuntimeLogs/LogEntry'; + +/** + * Registry API for the host-injected per-row action renderer used by + * the observability runtime-logs tables (`ObservabilityRuntimeLogs`, + * `ObservabilityProjectRuntimeLogs`). + * + * Under NFS, the host registers a `LogRowActionBlueprint` extension whose + * factory yields a `RenderLogRowAction`. This API's factory collects + * those extensions (via `inputs.renderers` on the alpha plugin) and + * exposes the first one as `render`. The logs components consume this + * API via `useApi(logRowActionRendererApiRef)`. + * + * When no extension is registered, `render` is a no-op that returns + * `null`, so the action column simply doesn't render. + */ +export interface LogRowActionRendererApi { + render: RenderLogRowAction; +} + +export const logRowActionRendererApiRef = createApiRef( + { + id: 'plugin.openchoreo-observability.log-row-action-renderer', + }, +); + +export class DefaultLogRowActionRendererApi implements LogRowActionRendererApi { + readonly render: RenderLogRowAction; + + private constructor(render: RenderLogRowAction) { + this.render = render; + } + + static create(options: { renderers: RenderLogRowAction[] }) { + const render: RenderLogRowAction = + options.renderers[0] ?? (() => null); + return new DefaultLogRowActionRendererApi(render); + } +} diff --git a/plugins/openchoreo-observability/src/api/index.ts b/plugins/openchoreo-observability/src/api/index.ts index 34fb8637f..77baa5b2f 100644 --- a/plugins/openchoreo-observability/src/api/index.ts +++ b/plugins/openchoreo-observability/src/api/index.ts @@ -19,3 +19,9 @@ export { type FinOpsAgentApi, type FinOpsRoutingContext, } from './FinOpsAgentApi'; + +export { + logRowActionRendererApiRef, + DefaultLogRowActionRendererApi, + type LogRowActionRendererApi, +} from './LogRowActionRendererApi'; diff --git a/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage.tsx b/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage.tsx index a02a108a9..ef3f75597 100644 --- a/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage.tsx +++ b/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Box, Typography, Button } from '@material-ui/core'; import { Progress } from '@backstage/core-components'; +import { useApiHolder } from '@backstage/core-plugin-api'; import { Alert } from '@material-ui/lab'; import { useEntity } from '@backstage/plugin-catalog-react'; import { CHOREO_ANNOTATIONS } from '@openchoreo/backstage-plugin-common'; @@ -20,6 +21,7 @@ import { import { useRuntimeLogsStyles } from './styles'; import { LogEntryField } from './types'; import type { RenderLogRowAction } from './LogEntry'; +import { logRowActionRendererApiRef } from '../../api/LogRowActionRendererApi'; export interface ObservabilityProjectRuntimeLogsPageProps { renderRowAction?: RenderLogRowAction; @@ -259,6 +261,14 @@ export const ObservabilityProjectRuntimeLogsPage = ({ permissionName, } = useLogsPermission(); + // Prop wins for legacy callers; under NFS, fall back to the + // host-registered renderer collected by the alpha plugin's + // logRowActionRendererApi. useApiHolder + get returns undefined when + // the API isn't registered, so legacy-only hosts stay no-op. + const apiHolder = useApiHolder(); + const effectiveRenderRowAction: RenderLogRowAction | undefined = + renderRowAction ?? apiHolder.get(logRowActionRendererApiRef)?.render; + if (permissionLoading) { return ; } @@ -274,6 +284,8 @@ export const ObservabilityProjectRuntimeLogsPage = ({ } return ( - + ); }; diff --git a/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityRuntimeLogsPage.tsx b/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityRuntimeLogsPage.tsx index 44df01637..00320a2f1 100644 --- a/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityRuntimeLogsPage.tsx +++ b/plugins/openchoreo-observability/src/components/RuntimeLogs/ObservabilityRuntimeLogsPage.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { Box, Typography, Button } from '@material-ui/core'; import { Progress } from '@backstage/core-components'; +import { useApiHolder } from '@backstage/core-plugin-api'; import { Alert } from '@material-ui/lab'; import { useEntity } from '@backstage/plugin-catalog-react'; import { CHOREO_ANNOTATIONS } from '@openchoreo/backstage-plugin-common'; @@ -20,6 +21,7 @@ import { import { useRuntimeLogsStyles } from './styles'; import { LOG_LEVELS } from './types'; import type { RenderLogRowAction } from './LogEntry'; +import { logRowActionRendererApiRef } from '../../api/LogRowActionRendererApi'; export interface ObservabilityRuntimeLogsPageProps { renderRowAction?: RenderLogRowAction; @@ -275,6 +277,14 @@ export const ObservabilityRuntimeLogsPage = ({ permissionName, } = useLogsPermission(); + // Prop wins for legacy callers; under NFS, fall back to the + // host-registered renderer collected by the alpha plugin's + // logRowActionRendererApi. useApiHolder + get returns undefined when + // the API isn't registered, so legacy-only hosts stay no-op. + const apiHolder = useApiHolder(); + const effectiveRenderRowAction: RenderLogRowAction | undefined = + renderRowAction ?? apiHolder.get(logRowActionRendererApiRef)?.render; + if (permissionLoading) { return ; } @@ -289,5 +299,9 @@ export const ObservabilityRuntimeLogsPage = ({ ); } - return ; + return ( + + ); }; diff --git a/plugins/openchoreo-observability/src/index.ts b/plugins/openchoreo-observability/src/index.ts index 9358cf5a0..f37c0ac32 100644 --- a/plugins/openchoreo-observability/src/index.ts +++ b/plugins/openchoreo-observability/src/index.ts @@ -12,3 +12,7 @@ export { ObservabilityCostAnalysis, } from './plugin'; export type { RenderLogRowAction } from './components/RuntimeLogs/LogEntry'; +export { + logRowActionRendererApiRef, + type LogRowActionRendererApi, +} from './api/LogRowActionRendererApi'; From 7759c2462236abfdbbed73988a70b2f96a7e28a9 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:02:31 +0530 Subject: [PATCH 18/44] fix(openchoreo-ci): scope Build tab to kind:component under NFS Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo-ci/src/alpha.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/openchoreo-ci/src/alpha.tsx b/plugins/openchoreo-ci/src/alpha.tsx index bae98d861..d144f97c0 100644 --- a/plugins/openchoreo-ci/src/alpha.tsx +++ b/plugins/openchoreo-ci/src/alpha.tsx @@ -26,6 +26,7 @@ const workflowsEntityContent = EntityContentBlueprint.make({ params: { path: '/workflows', title: 'Build', + filter: 'kind:component', loader: () => import('./components/Workflows').then(m => ), }, }); From 634da7160790242385776f98aa9dfda402009f48 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:18:39 +0530 Subject: [PATCH 19/44] refactor(openchoreo-react): move FeatureGatedContent next to FeatureGate Signed-off-by: Kavith Lokuhewage --- .../app/src/components/catalog/EntityPage.tsx | 2 +- .../FeatureGate}/FeatureGatedContent.tsx | 16 ++++++---------- .../src/components/FeatureGate/index.ts | 4 ++++ plugins/openchoreo-react/src/index.ts | 2 ++ 4 files changed, 13 insertions(+), 11 deletions(-) rename {packages/app/src/components/catalog => plugins/openchoreo-react/src/components/FeatureGate}/FeatureGatedContent.tsx (67%) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index a167cabf5..d8f30a9f8 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -139,10 +139,10 @@ import { import { FeatureGate, + FeatureGatedContent, CustomGraphNode, OpenChoreoEntityLayout, } from '@openchoreo/backstage-plugin-react'; -import { FeatureGatedContent } from './FeatureGatedContent'; import { WorkflowsOrExternalCICard } from './WorkflowsOrExternalCICard'; // External CI Platform imports diff --git a/packages/app/src/components/catalog/FeatureGatedContent.tsx b/plugins/openchoreo-react/src/components/FeatureGate/FeatureGatedContent.tsx similarity index 67% rename from packages/app/src/components/catalog/FeatureGatedContent.tsx rename to plugins/openchoreo-react/src/components/FeatureGate/FeatureGatedContent.tsx index 0f161f96a..aaf38a5d9 100644 --- a/packages/app/src/components/catalog/FeatureGatedContent.tsx +++ b/plugins/openchoreo-react/src/components/FeatureGate/FeatureGatedContent.tsx @@ -1,21 +1,17 @@ import { ReactNode } from 'react'; -import { useOpenChoreoFeatures } from '@openchoreo/backstage-plugin-react'; -import type { FeatureName } from '@openchoreo/backstage-plugin-common'; import { EmptyState } from '@backstage/core-components'; +import type { FeatureName } from '@openchoreo/backstage-plugin-common'; +import { useOpenChoreoFeatures } from '../../hooks/useOpenChoreoFeatures'; -interface FeatureGatedContentProps { +export interface FeatureGatedContentProps { feature: FeatureName; children: ReactNode; } /** - * Wrapper component for feature-gated route content. - * - * Unlike FeatureGate which conditionally renders children, - * this component always renders something (either the children or an empty state). - * This is required for routable extensions that must be present in the element tree. - * - * When the feature is disabled, shows an empty state message instead of the content. + * Routable variant of {@link FeatureGate}. Returns an {@link EmptyState} when + * the feature is disabled instead of `null`, so it remains valid as the body + * of a routable extension (`EntityContentBlueprint` loader, etc.). */ export function FeatureGatedContent({ feature, diff --git a/plugins/openchoreo-react/src/components/FeatureGate/index.ts b/plugins/openchoreo-react/src/components/FeatureGate/index.ts index 4a08f333d..8710ded36 100644 --- a/plugins/openchoreo-react/src/components/FeatureGate/index.ts +++ b/plugins/openchoreo-react/src/components/FeatureGate/index.ts @@ -3,3 +3,7 @@ export { withFeatureGate, type FeatureGateProps, } from './FeatureGate'; +export { + FeatureGatedContent, + type FeatureGatedContentProps, +} from './FeatureGatedContent'; diff --git a/plugins/openchoreo-react/src/index.ts b/plugins/openchoreo-react/src/index.ts index 1fc54f765..28eb8bf0d 100644 --- a/plugins/openchoreo-react/src/index.ts +++ b/plugins/openchoreo-react/src/index.ts @@ -8,8 +8,10 @@ export { SummaryWidgetWrapper } from './components/SummaryWidgetWrapper'; export { FeatureGate, + FeatureGatedContent, withFeatureGate, type FeatureGateProps, + type FeatureGatedContentProps, } from './components/FeatureGate'; export { AnnotationGate, From 027549b6c69734f0d0ac17a990056726a0cf0bee Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:26:07 +0530 Subject: [PATCH 20/44] feat(observability): register component-page entity tabs as NFS blueprints Signed-off-by: Kavith Lokuhewage --- .../openchoreo-observability/src/alpha.tsx | 104 +++++++++++++++++- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/plugins/openchoreo-observability/src/alpha.tsx b/plugins/openchoreo-observability/src/alpha.tsx index 45a157c97..fe425d102 100644 --- a/plugins/openchoreo-observability/src/alpha.tsx +++ b/plugins/openchoreo-observability/src/alpha.tsx @@ -5,6 +5,8 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/frontend-plugin-api'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import { FeatureGatedContent } from '@openchoreo/backstage-plugin-react'; import { rootRouteRef } from './routes'; import { @@ -85,13 +87,102 @@ const logRowActionRendererApi = ApiBlueprint.makeWithOverrides({ }, }); +/** + * Component-page entity tabs (kind:component). Each tab loads its page + * component lazily and wraps it in `FeatureGatedContent feature="observability"` + * so the tab is in-tree (so routing stays valid) but renders an + * empty-state when the host has observability disabled. + * + * The runtime-logs tab does NOT pass a `renderRowAction` prop — the page + * component reads the host-registered renderer through + * `useApiHolder().get(logRowActionRendererApiRef)` (see Step 1). + */ +const runtimeLogsEntityContent = EntityContentBlueprint.make({ + name: 'runtime-logs', + params: { + path: '/runtime-logs', + title: 'Logs', + filter: 'kind:component', + loader: () => + import('./components/RuntimeLogs/ObservabilityRuntimeLogsPage').then( + m => ( + + + + ), + ), + }, +}); + +const runtimeEventsEntityContent = EntityContentBlueprint.make({ + name: 'runtime-events', + params: { + path: '/runtime-events', + title: 'Events', + filter: 'kind:component', + loader: () => + import('./components/RuntimeEvents/ObservabilityRuntimeEventsPage').then( + m => ( + + + + ), + ), + }, +}); + +const metricsEntityContent = EntityContentBlueprint.make({ + name: 'metrics', + params: { + path: '/metrics', + title: 'Metrics', + filter: 'kind:component', + loader: () => + import('./components/Metrics/ObservabilityMetricsPage').then(m => ( + + + + )), + }, +}); + +const alertsEntityContent = EntityContentBlueprint.make({ + name: 'alerts', + params: { + path: '/alerts', + title: 'Alerts', + filter: 'kind:component', + loader: () => + import('./components/Alerts/ObservabilityAlertsPage').then(m => ( + + + + )), + }, +}); + +const wirelogsEntityContent = EntityContentBlueprint.make({ + name: 'wirelogs', + params: { + path: '/wirelogs', + title: 'Wirelogs', + filter: 'kind:component', + loader: () => + import('./components/Wirelogs/ObservabilityWirelogsPage').then(m => ( + + + + )), + }, +}); + /** * NFS entry point for the OpenChoreo Observability plugin. * - * Registers the three observability backend clients plus the - * log-row-action registry API. Entity tabs (Metrics, Traces, RCA, - * RuntimeLogs, etc.) ride through the legacy `src/plugin.ts` until - * a follow-up commit converts them to `EntityContentBlueprint`. + * Registers the three observability backend clients, the log-row-action + * registry API, and the component-page entity tabs (Logs, Events, + * Metrics, Alerts, Wirelogs). System-page tabs (ProjectRuntimeLogs, + * Traces, Incidents, RCA, CostAnalysis) follow in a separate commit. */ export default createFrontendPlugin({ pluginId: 'openchoreo-observability', @@ -101,5 +192,10 @@ export default createFrontendPlugin({ rcaAgentApi, finopsAgentApi, logRowActionRendererApi, + runtimeLogsEntityContent, + runtimeEventsEntityContent, + metricsEntityContent, + alertsEntityContent, + wirelogsEntityContent, ], }); From f0aa4ebb40dfd547a3a22926b68fc2aca81deaea Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:27:10 +0530 Subject: [PATCH 21/44] feat(observability): register system-page entity tabs as NFS blueprints Signed-off-by: Kavith Lokuhewage --- .../openchoreo-observability/src/alpha.tsx | 96 ++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/plugins/openchoreo-observability/src/alpha.tsx b/plugins/openchoreo-observability/src/alpha.tsx index fe425d102..726a59de3 100644 --- a/plugins/openchoreo-observability/src/alpha.tsx +++ b/plugins/openchoreo-observability/src/alpha.tsx @@ -176,13 +176,98 @@ const wirelogsEntityContent = EntityContentBlueprint.make({ }, }); +/** + * System-page (Project) entity tabs (kind:system). Same gating pattern + * as component-page tabs: lazy load + observability feature gate. The + * `/logs` tab uses `ObservabilityProjectRuntimeLogsPage` rather than the + * component-scoped runtime-logs page. + */ +const projectRuntimeLogsEntityContent = EntityContentBlueprint.make({ + name: 'project-runtime-logs', + params: { + path: '/logs', + title: 'Logs', + filter: 'kind:system', + loader: () => + import( + './components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage' + ).then(m => ( + + + + )), + }, +}); + +const tracesEntityContent = EntityContentBlueprint.make({ + name: 'traces', + params: { + path: '/traces', + title: 'Traces', + filter: 'kind:system', + loader: () => + import('./components/Traces/ObservabilityTracesPage').then(m => ( + + + + )), + }, +}); + +const projectIncidentsEntityContent = EntityContentBlueprint.make({ + name: 'project-incidents', + params: { + path: '/incidents', + title: 'Incidents', + filter: 'kind:system', + loader: () => + import('./components/Incidents/ObservabilityProjectIncidentsPage').then( + m => ( + + + + ), + ), + }, +}); + +const rcaReportsEntityContent = EntityContentBlueprint.make({ + name: 'rca-reports', + params: { + path: '/rca-reports', + title: 'RCA Reports', + filter: 'kind:system', + loader: () => + import('./components/RCA/RCAPage').then(m => ( + + + + )), + }, +}); + +const costAnalysisEntityContent = EntityContentBlueprint.make({ + name: 'cost-analysis', + params: { + path: '/cost-analysis', + title: 'Cost Analysis', + filter: 'kind:system', + loader: () => + import('./components/CostAnalysis').then(m => ( + + + + )), + }, +}); + /** * NFS entry point for the OpenChoreo Observability plugin. * * Registers the three observability backend clients, the log-row-action - * registry API, and the component-page entity tabs (Logs, Events, - * Metrics, Alerts, Wirelogs). System-page tabs (ProjectRuntimeLogs, - * Traces, Incidents, RCA, CostAnalysis) follow in a separate commit. + * registry API, the component-page entity tabs (Logs, Events, Metrics, + * Alerts, Wirelogs) and the system-page entity tabs (Logs, Traces, + * Incidents, RCA Reports, Cost Analysis). */ export default createFrontendPlugin({ pluginId: 'openchoreo-observability', @@ -197,5 +282,10 @@ export default createFrontendPlugin({ metricsEntityContent, alertsEntityContent, wirelogsEntityContent, + projectRuntimeLogsEntityContent, + tracesEntityContent, + projectIncidentsEntityContent, + rcaReportsEntityContent, + costAnalysisEntityContent, ], }); From b5270d508957b690d59e33f9883f8e2712df82a2 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:33:42 +0530 Subject: [PATCH 22/44] feat(openchoreo): register entity tabs and platform-kind cards as NFS blueprints Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo/src/alpha.tsx | 537 ++++++++++++++++++++++++++++++- 1 file changed, 528 insertions(+), 9 deletions(-) diff --git a/plugins/openchoreo/src/alpha.tsx b/plugins/openchoreo/src/alpha.tsx index 53fea616a..da35e0991 100644 --- a/plugins/openchoreo/src/alpha.tsx +++ b/plugins/openchoreo/src/alpha.tsx @@ -4,6 +4,12 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/frontend-plugin-api'; +import { + EntityCardBlueprint, + EntityContentBlueprint, +} from '@backstage/plugin-catalog-react/alpha'; +import { CHOREO_LABELS } from '@openchoreo/backstage-plugin-common'; +import { FeatureGate } from '@openchoreo/backstage-plugin-react'; import { rootCatalogEnvironmentRouteRef, @@ -24,17 +30,492 @@ const openChoreoClientApi = ApiBlueprint.make({ }), }); +// ─── Shared filter for any kind that needs the resource-definition tab ────── +// +// ResourceDefinitionTab is reused on ~20 entity kinds. Register it once with +// a callable filter (rather than 20 string-filter blueprints) so it stays +// a single registration in the alpha export and a single line in the +// extension array. +const KINDS_WITH_RESOURCE_DEFINITION = new Set([ + 'component', + 'system', + 'domain', + 'resource', + 'environment', + 'dataplane', + 'clusterdataplane', + 'workflowplane', + 'clusterworkflowplane', + 'observabilityplane', + 'clusterobservabilityplane', + 'deploymentpipeline', + 'componenttype', + 'resourcetype', + 'clustercomponenttype', + 'clusterresourcetype', + 'traittype', + 'clustertraittype', + 'workflow', + 'clusterworkflow', + 'componentworkflow', +]); + +const resourceDefinitionEntityContent = EntityContentBlueprint.make({ + name: 'resource-definition', + params: { + path: '/definition', + title: 'Definition', + filter: entity => + KINDS_WITH_RESOURCE_DEFINITION.has(entity.kind.toLowerCase()), + loader: () => + import('./components/ResourceDefinition').then(m => ( + + )), + }, +}); + +// ─── Component-page tabs (kind:component) ───────────────────────────────── +const componentDeployEntityContent = EntityContentBlueprint.make({ + name: 'component-deploy', + params: { + path: '/environments', + title: 'Deploy', + filter: 'kind:component', + loader: () => + import('./components/Environments/Environments').then(m => ( + + )), + }, +}); + +// ─── Component-page Overview cards (kind:component) ─────────────────────── +const deploymentStatusCard = EntityCardBlueprint.make({ + name: 'deployment-status', + params: { + filter: 'kind:component', + loader: () => + import('./components/Environments').then(m => ), + }, +}); + +// RuntimeHealthCard is observability-gated. FeatureGate (returns null when +// disabled) is the right wrapper because cards can vanish without breaking +// any route — unlike EntityContent, which must remain in tree. +const runtimeHealthCard = EntityCardBlueprint.make({ + name: 'runtime-health', + params: { + filter: 'kind:component', + loader: () => + import('./components/RuntimeLogs').then(m => ( + + + + )), + }, +}); + +// ─── System (project) page tabs + cards (kind:system) ───────────────────── +const cellDiagramEntityContent = EntityContentBlueprint.make({ + name: 'cell-diagram', + params: { + path: '/cell-diagram', + title: 'Cell Diagram', + filter: 'kind:system', + loader: () => + import('./components/CellDiagram/CellDiagram').then(m => ), + }, +}); + +const projectContentsCard = EntityCardBlueprint.make({ + name: 'project-contents', + params: { + filter: 'kind:system', + loader: () => + import('./components/Projects/ProjectContentsCard').then(m => ( + + )), + }, +}); + +const deploymentPipelineCard = EntityCardBlueprint.make({ + name: 'deployment-pipeline', + params: { + filter: 'kind:system', + loader: () => + import('./components/Projects/OverviewCards').then(m => ( + + )), + }, +}); + +// ─── Domain (namespace) page cards (kind:domain) ────────────────────────── +const namespaceProjectsCard = EntityCardBlueprint.make({ + name: 'namespace-projects', + params: { + filter: 'kind:domain', + loader: () => + import('./components/Namespaces').then(m => ), + }, +}); + +const namespaceResourcesCard = EntityCardBlueprint.make({ + name: 'namespace-resources', + params: { + filter: 'kind:domain', + loader: () => + import('./components/Namespaces').then(m => ), + }, +}); + +// ─── Resource page (managed) tab + cards ────────────────────────────────── +// +// Resources are kind:resource but only "OpenChoreo-managed" resources (a +// label-based discriminator) get this layout. Use a callable filter that +// matches on the CHOREO_LABELS.MANAGED label; consumers without the label +// fall through to upstream's default resource page. +const isOpenChoreoManagedResource = ( + entity: import('@backstage/catalog-model').Entity, +) => + entity.kind.toLowerCase() === 'resource' && + entity.metadata.labels?.[CHOREO_LABELS.MANAGED] === 'true'; + +const resourceDeployEntityContent = EntityContentBlueprint.make({ + name: 'resource-deploy', + params: { + path: '/environments', + title: 'Deploy', + filter: isOpenChoreoManagedResource, + loader: () => + import('./components/ResourceEnvironments').then(m => ( + + )), + }, +}); + +const resourceParametersCard = EntityCardBlueprint.make({ + name: 'resource-parameters', + params: { + filter: isOpenChoreoManagedResource, + loader: () => + import('./components/ResourceOverview').then(m => ( + + )), + }, +}); + +const resourceDeploymentsCard = EntityCardBlueprint.make({ + name: 'resource-deployments', + params: { + filter: isOpenChoreoManagedResource, + loader: () => + import('./components/ResourceOverview').then(m => ( + + )), + }, +}); + +const consumingComponentsCard = EntityCardBlueprint.make({ + name: 'consuming-components', + params: { + filter: isOpenChoreoManagedResource, + loader: () => + import('./components/ResourceOverview').then(m => ( + + )), + }, +}); + +// ─── Environment page cards (kind:environment) ──────────────────────────── +const environmentStatusSummaryCard = EntityCardBlueprint.make({ + name: 'environment-status-summary', + params: { + filter: 'kind:environment', + loader: () => + import('./components/EnvironmentOverview').then(m => ( + + )), + }, +}); + +const environmentPromotionCard = EntityCardBlueprint.make({ + name: 'environment-promotion', + params: { + filter: 'kind:environment', + loader: () => + import('./components/EnvironmentOverview').then(m => ( + + )), + }, +}); + +const environmentDeployedComponentsCard = EntityCardBlueprint.make({ + name: 'environment-deployed-components', + params: { + filter: 'kind:environment', + loader: () => + import('./components/EnvironmentOverview').then(m => ( + + )), + }, +}); + +const environmentGatewayConfigurationCard = EntityCardBlueprint.make({ + name: 'environment-gateway-configuration', + params: { + filter: 'kind:environment', + loader: () => + import('./components/EnvironmentOverview').then(m => ( + + )), + }, +}); + +// ─── Dataplane page cards (kind:dataplane) ──────────────────────────────── +const dataplaneStatusCard = EntityCardBlueprint.make({ + name: 'dataplane-status', + params: { + filter: 'kind:dataplane', + loader: () => + import('./components/DataplaneOverview').then(m => ( + + )), + }, +}); + +const dataplaneEnvironmentsCard = EntityCardBlueprint.make({ + name: 'dataplane-environments', + params: { + filter: 'kind:dataplane', + loader: () => + import('./components/DataplaneOverview').then(m => ( + + )), + }, +}); + +const dataplaneGatewayConfigurationCard = EntityCardBlueprint.make({ + name: 'dataplane-gateway-configuration', + params: { + filter: 'kind:dataplane', + loader: () => + import('./components/DataplaneOverview').then(m => ( + + )), + }, +}); + +// ─── ClusterDataplane page cards (kind:clusterdataplane) ────────────────── +const clusterDataplaneStatusCard = EntityCardBlueprint.make({ + name: 'cluster-dataplane-status', + params: { + filter: 'kind:clusterdataplane', + loader: () => + import('./components/ClusterDataplaneOverview').then(m => ( + + )), + }, +}); + +const clusterDataplaneEnvironmentsCard = EntityCardBlueprint.make({ + name: 'cluster-dataplane-environments', + params: { + filter: 'kind:clusterdataplane', + loader: () => + import('./components/ClusterDataplaneOverview').then(m => ( + + )), + }, +}); + +const clusterDataplaneGatewayConfigurationCard = EntityCardBlueprint.make({ + name: 'cluster-dataplane-gateway-configuration', + params: { + filter: 'kind:clusterdataplane', + loader: () => + import('./components/ClusterDataplaneOverview').then(m => ( + + )), + }, +}); + +// ─── WorkflowPlane / ClusterWorkflowPlane cards ─────────────────────────── +const workflowPlaneStatusCard = EntityCardBlueprint.make({ + name: 'workflow-plane-status', + params: { + filter: 'kind:workflowplane', + loader: () => + import('./components/WorkflowPlaneOverview').then(m => ( + + )), + }, +}); + +const clusterWorkflowPlaneStatusCard = EntityCardBlueprint.make({ + name: 'cluster-workflow-plane-status', + params: { + filter: 'kind:clusterworkflowplane', + loader: () => + import('./components/ClusterWorkflowPlaneOverview').then(m => ( + + )), + }, +}); + +// ─── ObservabilityPlane / ClusterObservabilityPlane cards ───────────────── +const observabilityPlaneStatusCard = EntityCardBlueprint.make({ + name: 'observability-plane-status', + params: { + filter: 'kind:observabilityplane', + loader: () => + import('./components/ObservabilityPlaneOverview').then(m => ( + + )), + }, +}); + +const observabilityPlaneLinkedPlanesCard = EntityCardBlueprint.make({ + name: 'observability-plane-linked-planes', + params: { + filter: 'kind:observabilityplane', + loader: () => + import('./components/ObservabilityPlaneOverview').then(m => ( + + )), + }, +}); + +const clusterObservabilityPlaneStatusCard = EntityCardBlueprint.make({ + name: 'cluster-observability-plane-status', + params: { + filter: 'kind:clusterobservabilityplane', + loader: () => + import('./components/ClusterObservabilityPlaneOverview').then(m => ( + + )), + }, +}); + +const clusterObservabilityPlaneLinkedPlanesCard = EntityCardBlueprint.make({ + name: 'cluster-observability-plane-linked-planes', + params: { + filter: 'kind:clusterobservabilityplane', + loader: () => + import('./components/ClusterObservabilityPlaneOverview').then(m => ( + + )), + }, +}); + +// ─── DeploymentPipeline page cards (kind:deploymentpipeline) ────────────── +const deploymentPipelineVisualizationCard = EntityCardBlueprint.make({ + name: 'deployment-pipeline-visualization', + params: { + filter: 'kind:deploymentpipeline', + loader: () => + import('./components/DeploymentPipelineOverview').then(m => ( + + )), + }, +}); + +const promotionPathsCard = EntityCardBlueprint.make({ + name: 'promotion-paths', + params: { + filter: 'kind:deploymentpipeline', + loader: () => + import('./components/DeploymentPipelineOverview').then(m => ( + + )), + }, +}); + +// ─── *Type overview cards (componenttype / resourcetype / traittype) ────── +// +// ComponentTypeOverviewCard is reused on kind:componenttype AND +// kind:clustercomponenttype — register once with a multi-kind callable +// filter rather than two near-identical blueprints. Same shape for the +// resource-type and trait-type variants. +const componentTypeOverviewCard = EntityCardBlueprint.make({ + name: 'component-type-overview', + params: { + filter: entity => + ['componenttype', 'clustercomponenttype'].includes( + entity.kind.toLowerCase(), + ), + loader: () => + import('./components/ComponentTypeOverview').then(m => ( + + )), + }, +}); + +const resourceTypeOverviewCard = EntityCardBlueprint.make({ + name: 'resource-type-overview', + params: { + filter: entity => + ['resourcetype', 'clusterresourcetype'].includes( + entity.kind.toLowerCase(), + ), + loader: () => + import('./components/ResourceTypeOverview').then(m => ( + + )), + }, +}); + +const traitTypeOverviewCard = EntityCardBlueprint.make({ + name: 'trait-type-overview', + params: { + filter: entity => + ['traittype', 'clustertraittype'].includes(entity.kind.toLowerCase()), + loader: () => + import('./components/TraitTypeOverview').then(m => ( + + )), + }, +}); + +// ─── Workflow / ClusterWorkflow / ComponentWorkflow overview cards ──────── +const workflowOverviewCard = EntityCardBlueprint.make({ + name: 'workflow-overview', + params: { + filter: entity => + ['workflow', 'clusterworkflow'].includes(entity.kind.toLowerCase()), + loader: () => + import('./components/WorkflowOverview').then(m => ( + + )), + }, +}); + +const componentWorkflowOverviewCard = EntityCardBlueprint.make({ + name: 'component-workflow-overview', + params: { + filter: 'kind:componentworkflow', + loader: () => + import('./components/ComponentWorkflowOverview').then(m => ( + + )), + }, +}); + /** * NFS entry point for the OpenChoreo plugin. * - * Registers the OpenChoreoClient API. The four legacy routable extensions - * (Environments, ResourceEnvironments, CellDiagram, AccessControlPage) and - * four component cards (WorkflowsOverviewCard, DeploymentStatusCard, - * RuntimeHealthCard, DeploymentPipelineCard) continue to flow through - * src/plugin.ts because the host app mounts them as plain React components - * inside legacy EntityLayout.Route/Card structures. Wire them as - * EntityContentBlueprint / EntityCardBlueprint extensions here when the host - * moves to NFS-driven entity tabs. + * Registers the OpenChoreoClient API, the cross-kind ResourceDefinitionTab, + * the component-page Deploy tab + DeploymentStatus/RuntimeHealth cards, the + * system-page Cell Diagram tab + ProjectContents/DeploymentPipeline cards, + * the domain-page Namespace cards, the managed-resource Deploy tab + cards, + * and the per-kind overview cards for every OpenChoreo platform kind + * (Environment, DataPlane/ClusterDataPlane, WorkflowPlane/ClusterWorkflowPlane, + * ObservabilityPlane/ClusterObservabilityPlane, DeploymentPipeline, + * ComponentType/ResourceType/TraitType + cluster variants, + * Workflow/ClusterWorkflow/ComponentWorkflow). + * + * Host-only mounts (OpenChoreoAboutCard, EntityCatalogGraphCard with custom + * relations, WorkflowsOrExternalCICard, the Overview FailedBuildSnackbar) stay + * in `packages/app` and ride through `customAppModule` because they belong + * to the host's composition layer, not to this plugin. */ export default createFrontendPlugin({ pluginId: 'openchoreo', @@ -43,5 +524,43 @@ export default createFrontendPlugin({ accessControl: accessControlRouteRef, resourceEnvironments: resourceEnvironmentsRouteRef, }, - extensions: [openChoreoClientApi], + extensions: [ + openChoreoClientApi, + resourceDefinitionEntityContent, + componentDeployEntityContent, + deploymentStatusCard, + runtimeHealthCard, + cellDiagramEntityContent, + projectContentsCard, + deploymentPipelineCard, + namespaceProjectsCard, + namespaceResourcesCard, + resourceDeployEntityContent, + resourceParametersCard, + resourceDeploymentsCard, + consumingComponentsCard, + environmentStatusSummaryCard, + environmentPromotionCard, + environmentDeployedComponentsCard, + environmentGatewayConfigurationCard, + dataplaneStatusCard, + dataplaneEnvironmentsCard, + dataplaneGatewayConfigurationCard, + clusterDataplaneStatusCard, + clusterDataplaneEnvironmentsCard, + clusterDataplaneGatewayConfigurationCard, + workflowPlaneStatusCard, + clusterWorkflowPlaneStatusCard, + observabilityPlaneStatusCard, + observabilityPlaneLinkedPlanesCard, + clusterObservabilityPlaneStatusCard, + clusterObservabilityPlaneLinkedPlanesCard, + deploymentPipelineVisualizationCard, + promotionPathsCard, + componentTypeOverviewCard, + resourceTypeOverviewCard, + traitTypeOverviewCard, + workflowOverviewCard, + componentWorkflowOverviewCard, + ], }); From 87241e12087d533287803356d522228cc08412b2 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 11:35:16 +0530 Subject: [PATCH 23/44] feat(openchoreo-workflows): register Workflow Runs entity tab as NFS blueprint Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo-workflows/src/alpha.tsx | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/plugins/openchoreo-workflows/src/alpha.tsx b/plugins/openchoreo-workflows/src/alpha.tsx index 3e9b02612..3f5256628 100644 --- a/plugins/openchoreo-workflows/src/alpha.tsx +++ b/plugins/openchoreo-workflows/src/alpha.tsx @@ -5,6 +5,7 @@ import { fetchApiRef, PageBlueprint, } from '@backstage/frontend-plugin-api'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { rootRouteRef } from './routes'; import { genericWorkflowsClientApiRef } from './api/GenericWorkflowsClientApi'; @@ -33,11 +34,41 @@ const genericWorkflowsPage = PageBlueprint.make({ }, }); +/** + * Workflow Runs entity tab — mounts under workflow and clusterworkflow + * kinds, but only for entries with `spec.type === 'Generic'`. The + * EntityNamespaceProvider supplies the entity's namespace to the runs + * table via React context, matching the legacy `EntityPage.tsx` mount. + */ +const workflowRunsEntityContent = EntityContentBlueprint.make({ + name: 'workflow-runs', + params: { + path: '/runs', + title: 'Runs', + filter: entity => + ['workflow', 'clusterworkflow'].includes(entity.kind.toLowerCase()) && + (entity.spec as { type?: string } | undefined)?.type === 'Generic', + loader: () => + Promise.all([ + import('./components/WorkflowRunsContent'), + import('./components/EntityNamespaceProvider'), + ]).then(([runs, provider]) => ( + + + + )), + }, +}); + /** * NFS entry point for the OpenChoreo generic-workflows plugin. */ export default createFrontendPlugin({ pluginId: 'openchoreo-workflows', routes: { root: rootRouteRef }, - extensions: [genericWorkflowsClientApi, genericWorkflowsPage], + extensions: [ + genericWorkflowsClientApi, + genericWorkflowsPage, + workflowRunsEntityContent, + ], }); From 6d6839cb7ce37f186a8fb5de005b9331a0411ca4 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 12:17:57 +0530 Subject: [PATCH 24/44] feat(app): register host overview cards as NFS EntityCardBlueprints Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis/customOverrides.tsx | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index d7ac7969c..72baf7839 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -62,6 +62,8 @@ import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; import { openChoreoTokenDecorator } from '../scaffolder/openChoreoTokenDecorator'; import { LogRowActionBlueprint } from '@openchoreo/backstage-plugin-openchoreo-observability/alpha'; import { InvestigateLogButton } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import type { Entity } from '@backstage/catalog-model'; /** * Override `catalog-graph`'s default `api:catalog-graph` to include the @@ -216,6 +218,59 @@ export const customAppModule = createFrontendModule({ ), }, }), + // OpenChoreoAboutCard sits in the Overview "info" slot on every + // OpenChoreo-relevant entity kind. Lives in `packages/app` (not in + // the openchoreo plugin) because the About card is the host's + // composition of upstream's About metadata + OpenChoreo-specific + // edit affordances — a host concern, not a plugin one. + EntityCardBlueprint.make({ + name: 'openchoreo-about', + params: { + type: 'info', + filter: (entity: Entity) => + [ + 'component', + 'system', + 'domain', + 'resource', + 'environment', + 'dataplane', + 'clusterdataplane', + 'workflowplane', + 'clusterworkflowplane', + 'observabilityplane', + 'clusterobservabilityplane', + 'deploymentpipeline', + 'componenttype', + 'resourcetype', + 'clustercomponenttype', + 'clusterresourcetype', + 'traittype', + 'clustertraittype', + 'workflow', + 'clusterworkflow', + 'componentworkflow', + ].includes(entity.kind.toLowerCase()), + loader: () => + import('../components/catalog/OpenChoreoAboutCard').then(m => ( + + )), + }, + }), + // CI status card — internally branches between the OpenChoreo + // WorkflowsOverviewCard and an external-CI card (Jenkins / GitHub + // Actions / GitLab) based on the entity's CI annotation. Component + // pages only. + EntityCardBlueprint.make({ + name: 'workflows-or-external-ci', + params: { + filter: 'kind:component', + loader: () => + import('../components/catalog/WorkflowsOrExternalCICard').then(m => ( + + )), + }, + }), ], }); From bf2ac842d50e3f78a44eb23b900f7d4cd12038da Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 14:04:06 +0530 Subject: [PATCH 25/44] feat(app): own /catalog/.../entity via NFS page:catalog/entity override Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 20 ++--- packages/app/src/apis/customOverrides.tsx | 78 +++++++++++++++++- .../catalog/OpenChoreoCatalogEntityPage.tsx | 82 +++++++++++++++++++ 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 4f041c1fa..f6958816b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,10 +1,6 @@ import { Route } from 'react-router-dom'; import { apiDocsPlugin } from '@backstage/plugin-api-docs'; -import { - CatalogEntityPage, - CatalogIndexPage, - catalogPlugin, -} from '@backstage/plugin-catalog'; +import { CatalogIndexPage, catalogPlugin } from '@backstage/plugin-catalog'; import { CatalogImportPage, catalogImportPlugin, @@ -51,7 +47,6 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { apis } from './apis'; -import { entityPage } from './components/catalog/EntityPage'; import { CustomCatalogPage } from './components/catalog/CustomCatalogPage'; import { CustomApiExplorerPage } from './components/catalog/CustomApiExplorerPage'; import { searchPage } from './components/search/SearchPage'; @@ -153,12 +148,13 @@ const routes = ( }> - } - > - {entityPage} - + {/* + The entity route (`/catalog/:namespace/:kind/:name`) is owned by the + NFS `page:catalog/entity` extension — see customOverrides.tsx where + we override its loader to wrap upstream's NFS `inputs.contents` + accumulation in `` for the OpenChoreo + header, menus, and existence checks. + */} } /> ` children rather than upstream's + * ``. */ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ extensions: [ @@ -155,6 +168,67 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ }), }), }), + catalogPluginAlphaBase.getExtension('page:catalog/entity').override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + params: { + loader: async () => { + const [{ OpenChoreoCatalogEntityPage, OpenChoreoEntityLayout }] = + await Promise.all([ + import('../components/catalog/OpenChoreoCatalogEntityPage'), + ]); + const buildFilterFn = ( + fn: ((entity: Entity) => boolean) | undefined, + expr: string | undefined, + ) => { + if (fn) return fn; + if (expr) { + // Minimal "kind:foo" / "kind:foo,kind:bar" parser to keep the + // override self-contained; matches upstream's + // `buildFilterFn` semantics for the only expression shapes + // we emit in our Step 2 blueprints. + const kinds = expr + .split(',') + .map(p => p.trim()) + .filter(p => p.startsWith('kind:')) + .map(p => p.slice('kind:'.length).toLowerCase()); + return (entity: Entity) => + kinds.includes(entity.kind.toLowerCase()); + } + return undefined; + }; + const Component = () => ( + + {inputs.contents.map(output => { + const path = output.get(coreExtensionData.routePath); + return ( + + {output.get(coreExtensionData.reactElement)} + + ); + })} + + ); + return ; + }, + }, + }); + }, + }), ], }); diff --git a/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx new file mode 100644 index 000000000..8edd9d960 --- /dev/null +++ b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx @@ -0,0 +1,82 @@ +import type { ReactNode } from 'react'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useAsyncRetry from 'react-use/esm/useAsyncRetry'; +import type { Entity } from '@backstage/catalog-model'; +import { errorApiRef, useApi, useRouteRefParams } from '@backstage/core-plugin-api'; +import { + AsyncEntityProvider, + catalogApiRef, + entityRouteRef, + type EntityLoadingStatus, +} from '@backstage/plugin-catalog-react'; +import { OpenChoreoEntityLayout } from '@openchoreo/backstage-plugin-react'; +import { EntityLayoutWithDelete } from './EntityLayoutWithDelete'; + +/** + * Local copy of upstream's internal `useEntityFromUrl` hook (not part of + * `@backstage/plugin-catalog`'s public surface — see + * `node_modules/@backstage/plugin-catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts`). + * Inlined here so the NFS `page:catalog/entity` override can mount its own + * `AsyncEntityProvider` without relying on a private import. + */ +function useEntityFromUrl(): EntityLoadingStatus { + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); + const navigate = useNavigate(); + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { + value: entity, + error, + loading, + retry: refresh, + } = useAsyncRetry( + () => + catalogApi.getEntityByRef({ kind, namespace, name }) as Promise< + Entity | undefined + >, + [catalogApi, kind, namespace, name], + ); + + useEffect(() => { + if (!name) { + errorApi.post(new Error('No name provided!')); + navigate('/'); + } + }, [errorApi, navigate, error, loading, entity, name]); + + return { entity, loading, error, refresh }; +} + +interface OpenChoreoCatalogEntityPageProps { + /** + * The route children to render inside the layout. The NFS + * `page:catalog/entity` loader maps `inputs.contents` to + * `` elements and passes them here. + */ + children: ReactNode; +} + +/** + * Replacement for the host's legacy `` mount, used by + * the NFS `page:catalog/entity` extension override. Provides the + * `AsyncEntityProvider` (which upstream's loader does for its own + * `EntityLayout`) and delegates the layout to {@link EntityLayoutWithDelete} + * so the OpenChoreo header, delete + edit-annotations menu items, and + * existence-check empty states are preserved when the host migrates off + * the legacy `FlatRoutes` entity mount. + */ +export function OpenChoreoCatalogEntityPage({ + children, +}: OpenChoreoCatalogEntityPageProps) { + return ( + + {children} + + ); +} + +// Re-export so the override loader (which can't usefully import a JSX +// runtime component from a deep React module) has a single entry. +export { OpenChoreoEntityLayout }; From 1cd3e78fd462972c54b93f08ae0b0e928308f46b Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 14:42:02 +0530 Subject: [PATCH 26/44] Revert "feat(app): own /catalog/.../entity via NFS page:catalog/entity override" This reverts commit bf2ac842d50e3f78a44eb23b900f7d4cd12038da. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 20 +++-- packages/app/src/apis/customOverrides.tsx | 78 +----------------- .../catalog/OpenChoreoCatalogEntityPage.tsx | 82 ------------------- 3 files changed, 14 insertions(+), 166 deletions(-) delete mode 100644 packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f6958816b..4f041c1fa 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,6 +1,10 @@ import { Route } from 'react-router-dom'; import { apiDocsPlugin } from '@backstage/plugin-api-docs'; -import { CatalogIndexPage, catalogPlugin } from '@backstage/plugin-catalog'; +import { + CatalogEntityPage, + CatalogIndexPage, + catalogPlugin, +} from '@backstage/plugin-catalog'; import { CatalogImportPage, catalogImportPlugin, @@ -47,6 +51,7 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { apis } from './apis'; +import { entityPage } from './components/catalog/EntityPage'; import { CustomCatalogPage } from './components/catalog/CustomCatalogPage'; import { CustomApiExplorerPage } from './components/catalog/CustomApiExplorerPage'; import { searchPage } from './components/search/SearchPage'; @@ -148,13 +153,12 @@ const routes = ( }> - {/* - The entity route (`/catalog/:namespace/:kind/:name`) is owned by the - NFS `page:catalog/entity` extension — see customOverrides.tsx where - we override its loader to wrap upstream's NFS `inputs.contents` - accumulation in `` for the OpenChoreo - header, menus, and existence checks. - */} + } + > + {entityPage} + } /> ` children rather than upstream's - * ``. */ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ extensions: [ @@ -168,67 +155,6 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ }), }), }), - catalogPluginAlphaBase.getExtension('page:catalog/entity').override({ - factory(originalFactory, { inputs }) { - return originalFactory({ - params: { - loader: async () => { - const [{ OpenChoreoCatalogEntityPage, OpenChoreoEntityLayout }] = - await Promise.all([ - import('../components/catalog/OpenChoreoCatalogEntityPage'), - ]); - const buildFilterFn = ( - fn: ((entity: Entity) => boolean) | undefined, - expr: string | undefined, - ) => { - if (fn) return fn; - if (expr) { - // Minimal "kind:foo" / "kind:foo,kind:bar" parser to keep the - // override self-contained; matches upstream's - // `buildFilterFn` semantics for the only expression shapes - // we emit in our Step 2 blueprints. - const kinds = expr - .split(',') - .map(p => p.trim()) - .filter(p => p.startsWith('kind:')) - .map(p => p.slice('kind:'.length).toLowerCase()); - return (entity: Entity) => - kinds.includes(entity.kind.toLowerCase()); - } - return undefined; - }; - const Component = () => ( - - {inputs.contents.map(output => { - const path = output.get(coreExtensionData.routePath); - return ( - - {output.get(coreExtensionData.reactElement)} - - ); - })} - - ); - return ; - }, - }, - }); - }, - }), ], }); diff --git a/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx deleted file mode 100644 index 8edd9d960..000000000 --- a/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import type { ReactNode } from 'react'; -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import useAsyncRetry from 'react-use/esm/useAsyncRetry'; -import type { Entity } from '@backstage/catalog-model'; -import { errorApiRef, useApi, useRouteRefParams } from '@backstage/core-plugin-api'; -import { - AsyncEntityProvider, - catalogApiRef, - entityRouteRef, - type EntityLoadingStatus, -} from '@backstage/plugin-catalog-react'; -import { OpenChoreoEntityLayout } from '@openchoreo/backstage-plugin-react'; -import { EntityLayoutWithDelete } from './EntityLayoutWithDelete'; - -/** - * Local copy of upstream's internal `useEntityFromUrl` hook (not part of - * `@backstage/plugin-catalog`'s public surface — see - * `node_modules/@backstage/plugin-catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts`). - * Inlined here so the NFS `page:catalog/entity` override can mount its own - * `AsyncEntityProvider` without relying on a private import. - */ -function useEntityFromUrl(): EntityLoadingStatus { - const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - const navigate = useNavigate(); - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const { - value: entity, - error, - loading, - retry: refresh, - } = useAsyncRetry( - () => - catalogApi.getEntityByRef({ kind, namespace, name }) as Promise< - Entity | undefined - >, - [catalogApi, kind, namespace, name], - ); - - useEffect(() => { - if (!name) { - errorApi.post(new Error('No name provided!')); - navigate('/'); - } - }, [errorApi, navigate, error, loading, entity, name]); - - return { entity, loading, error, refresh }; -} - -interface OpenChoreoCatalogEntityPageProps { - /** - * The route children to render inside the layout. The NFS - * `page:catalog/entity` loader maps `inputs.contents` to - * `` elements and passes them here. - */ - children: ReactNode; -} - -/** - * Replacement for the host's legacy `` mount, used by - * the NFS `page:catalog/entity` extension override. Provides the - * `AsyncEntityProvider` (which upstream's loader does for its own - * `EntityLayout`) and delegates the layout to {@link EntityLayoutWithDelete} - * so the OpenChoreo header, delete + edit-annotations menu items, and - * existence-check empty states are preserved when the host migrates off - * the legacy `FlatRoutes` entity mount. - */ -export function OpenChoreoCatalogEntityPage({ - children, -}: OpenChoreoCatalogEntityPageProps) { - return ( - - {children} - - ); -} - -// Re-export so the override loader (which can't usefully import a JSX -// runtime component from a deep React module) has a single entry. -export { OpenChoreoEntityLayout }; From fa29ba340f4d289a32bc50961c04ca4b7a3e48fe Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 14:43:50 +0530 Subject: [PATCH 27/44] fix(app): reorder createApp features so NFS overrides win over legacy api re-emission Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 4f041c1fa..aa88bdc76 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -262,7 +262,16 @@ const legacyRoot = convertLegacyAppRoot( const app = createApp({ features: [ + // `...legacyRoot` re-emits each legacy plugin's `apis: [...]` array as + // ApiBlueprint extensions under the legacy plugin's own pluginId + // (collectLegacyRoutes). The NFS api-factory registry resolves + // same-pluginId factories last-write-wins, so the override features + // below MUST come after `...legacyRoot` to win the contest. Otherwise + // our custom catalog-graph relations, entity-presentation kind icons, + // and scaffolder form-decorator override get silently overwritten by + // upstream defaults at startup. legacyAppOptions, + ...legacyRoot, customAppModule, upstreamScaffolderPluginAlpha, catalogGraphPluginAlpha, @@ -273,7 +282,6 @@ const app = createApp({ openchoreoObservabilityPluginAlpha, openchoreoWorkflowsPluginAlpha, platformEngineerCorePluginAlpha, - ...legacyRoot, ], bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { From 21f47e76395fa42acac1276be01c6c8b476c52a2 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:00:45 +0530 Subject: [PATCH 28/44] fix(app): override page:catalog loader for CustomCatalogPage and unblock scaffolder.root binding Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis/customOverrides.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index 72baf7839..daa786dab 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -119,9 +119,24 @@ export const catalogGraphPluginAlpha = * Override `catalog`'s default `api:catalog/entity-presentation` to provide * kind icons for OpenChoreo-specific entity kinds (Environment, DataPlane, * DeploymentPipeline, etc.) in the catalog graph and entity views. + * + * Also overrides `page:catalog` so /catalog renders the host's + * `CustomCatalogPage` (kind-grouped picker + card grid layout) instead of + * upstream's `DefaultCatalogPage`. Before the createApp feature reorder + * landed, the legacy `` mount won; under the + * reorder, upstream's NFS extension wins by default — so we override its + * loader explicitly. */ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ extensions: [ + catalogPluginAlphaBase.getExtension('page:catalog').override({ + params: { + loader: () => + import('../components/catalog/CustomCatalogPage').then(m => ( + + )), + }, + }), catalogPluginAlphaBase .getExtension('api:catalog/entity-presentation') .override({ @@ -276,9 +291,6 @@ export const customAppModule = createFrontendModule({ export const scaffolderPluginAlpha = scaffolderPluginAlphaBase.withOverrides({ extensions: [ - scaffolderPluginAlphaBase - .getExtension('page:scaffolder') - .override({ disabled: true }), scaffolderPluginAlphaBase .getExtension('api:scaffolder/form-decorators') .override({ From e68b94cf5fdcc07188a1dbc9c968477f1e743a7e Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:04:20 +0530 Subject: [PATCH 29/44] fix(app): override page:scaffolder loader to render OpenChoreoScaffolderPage Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis/customOverrides.tsx | 14 +++ .../scaffolder/OpenChoreoScaffolderPage.tsx | 87 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index daa786dab..4c33d8972 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -291,6 +291,20 @@ export const customAppModule = createFrontendModule({ export const scaffolderPluginAlpha = scaffolderPluginAlphaBase.withOverrides({ extensions: [ + // Override `page:scaffolder`'s loader to render the host's + // `` composition (CustomTemplateListPage, + // CustomReviewStep, and 27 field extensions). Same reason as the + // `page:catalog` override above: the createApp feature reorder lets + // upstream's NFS scaffolder page win by default, which would drop + // every host customization. + scaffolderPluginAlphaBase.getExtension('page:scaffolder').override({ + params: { + loader: () => + import('../components/scaffolder/OpenChoreoScaffolderPage').then( + m => , + ), + }, + }), scaffolderPluginAlphaBase .getExtension('api:scaffolder/form-decorators') .override({ diff --git a/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx b/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx new file mode 100644 index 000000000..c4af9a5ab --- /dev/null +++ b/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx @@ -0,0 +1,87 @@ +import { ScaffolderPage } from '@backstage/plugin-scaffolder'; +import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react'; +import { ComponentNamePickerFieldExtension } from '../../scaffolder/ComponentNamePicker'; +import { ResourceNamePickerFieldExtension } from '../../scaffolder/ResourceNamePicker'; +import { BuildTemplatePickerFieldExtension } from '../../scaffolder/BuildTemplatePicker'; +import { BuildTemplateParametersFieldExtension } from '../../scaffolder/BuildTemplateParameters'; +import { BuildWorkflowPickerFieldExtension } from '../../scaffolder/BuildWorkflowPicker'; +import { BuildWorkflowParametersFieldExtension } from '../../scaffolder/BuildWorkflowParameters'; +import { TraitsFieldExtension } from '../../scaffolder/TraitsField'; +import { SwitchFieldExtension } from '../../scaffolder/SwitchField'; +import { AdvancedConfigurationFieldExtension } from '../../scaffolder/AdvancedConfigurationField'; +import { DeploymentSourcePickerFieldExtension } from '../../scaffolder/DeploymentSourcePicker'; +import { BuildAndDeployFieldExtension } from '../../scaffolder/BuildAndDeployField'; +import { ContainerImageFieldExtension } from '../../scaffolder/ContainerImageField'; +import { ComponentTypeYamlEditorFieldExtension } from '../../scaffolder/ComponentTypeYamlEditor'; +import { TraitYamlEditorFieldExtension } from '../../scaffolder/TraitYamlEditor'; +import { ClusterComponentTypeYamlEditorFieldExtension } from '../../scaffolder/ClusterComponentTypeYamlEditor'; +import { ClusterResourceTypeYamlEditorFieldExtension } from '../../scaffolder/ClusterResourceTypeYamlEditor'; +import { ResourceTypeYamlEditorFieldExtension } from '../../scaffolder/ResourceTypeYamlEditor'; +import { ResourceParametersFieldExtension } from '../../scaffolder/ResourceParametersField'; +import { ClusterTraitYamlEditorFieldExtension } from '../../scaffolder/ClusterTraitYamlEditor'; +import { ComponentWorkflowYamlEditorFieldExtension } from '../../scaffolder/ComponentWorkflowYamlEditor'; +import { ClusterWorkflowYamlEditorFieldExtension } from '../../scaffolder/ClusterWorkflowYamlEditor'; +import { GitSourceFieldExtension } from '../../scaffolder/GitSourceField'; +import { ProjectNamespaceFieldExtension } from '../../scaffolder/ProjectNamespaceField'; +import { NamespaceEntityPickerFieldExtension } from '../../scaffolder/NamespaceEntityPicker'; +import { DeploymentPipelinePickerFieldExtension } from '../../scaffolder/DeploymentPipelinePicker'; +import { EnvironmentFormWithYamlFieldExtension } from '../../scaffolder/EnvironmentFormWithYaml'; +import { DeploymentPipelineFormWithYamlFieldExtension } from '../../scaffolder/DeploymentPipelineFormWithYaml'; +import { WorkloadDetailsFieldExtension } from '../../scaffolder/WorkloadDetailsField'; +import { CustomTemplateListPage } from './CustomTemplateListPage'; +import { CustomReviewStep } from '../../scaffolder/CustomReviewState'; + +/** + * Host's scaffolder page composition — `` with the + * OpenChoreo header copy, the CustomTemplateListPage / CustomReviewStep + * component overrides, and all 27 field-extension children. Used by the + * `page:scaffolder` override in customOverrides.tsx so /create renders + * through NFS without losing any of the host customizations the legacy + * `...}>` mount supplied. + */ +export function OpenChoreoScaffolderPage() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} From 0499eeaa3479a490c4e73c77e8b70e7bd67d847e Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:09:22 +0530 Subject: [PATCH 30/44] fix(scaffolder): preserve inputs.formDecorators in app override Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis/customOverrides.tsx | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index 4c33d8972..ee291f6da 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -32,6 +32,7 @@ import { formDecoratorsApiRef, DefaultScaffolderFormDecoratorsApi, } from '@backstage/plugin-scaffolder/alpha'; +import { FormDecoratorBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; import { RELATION_DEPLOYS_TO, RELATION_DEPLOYED_BY, @@ -305,18 +306,31 @@ export const scaffolderPluginAlpha = scaffolderPluginAlphaBase.withOverrides({ ), }, }), + // Inject the OpenChoreo IDP-token decorator alongside any other + // FormDecoratorBlueprint extensions plugins may contribute. The + // earlier shape (`params: defineParams => defineParams({ factory: () => create({decorators: [openChoreoTokenDecorator]}) })`) + // discarded `inputs.formDecorators`, silently dropping every other + // plugin's decorator. Using `factory(originalFactory, { inputs })` + // preserves the upstream accumulation and then concats ours. scaffolderPluginAlphaBase .getExtension('api:scaffolder/form-decorators') .override({ - params: defineParams => - defineParams({ - api: formDecoratorsApiRef, - deps: {}, - factory: () => - DefaultScaffolderFormDecoratorsApi.create({ - decorators: [openChoreoTokenDecorator], + factory(originalFactory, { inputs }) { + const contributed = inputs.formDecorators.map(e => + e.get(FormDecoratorBlueprint.dataRefs.formDecoratorLoader), + ); + return originalFactory({ + params: defineParams => + defineParams({ + api: formDecoratorsApiRef, + deps: {}, + factory: () => + DefaultScaffolderFormDecoratorsApi.create({ + decorators: [openChoreoTokenDecorator, ...contributed], + }), }), - }), + }); + }, }), ], }); From 3d951324e6b7d41504ca344d55dac8e5ba4bf3e2 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:26:01 +0530 Subject: [PATCH 31/44] fix(app): mount DependencyGraphZoomOverrides under Root and restore ScaffolderLayout Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 2 -- packages/app/src/components/Root/Root.tsx | 9 +++++++ .../scaffolder/OpenChoreoScaffolderPage.tsx | 27 ++++++++++--------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index aa88bdc76..a225bfc04 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -120,7 +120,6 @@ import ExtensionIcon from '@material-ui/icons/Extension'; import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; import { VisitListener } from '@backstage/plugin-home'; -import { DependencyGraphZoomOverrides } from './components/graph/DependencyGraphZoomOverrides'; const legacyAppOptions = convertLegacyAppOptions({ apis, @@ -252,7 +251,6 @@ const legacyRoot = convertLegacyAppRoot( <> - {routes} diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index ae57a84d8..60a42e529 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -40,6 +40,7 @@ import CategoryIcon from '@material-ui/icons/Category'; import BubbleChartIcon from '@material-ui/icons/BubbleChart'; import { AssistantDrawerProvider } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; import { ScaffolderPreselectionProvider } from '../../scaffolder/ScaffolderPreselectionContext'; +import { DependencyGraphZoomOverrides } from '../graph/DependencyGraphZoomOverrides'; const isMac = typeof navigator !== 'undefined' && @@ -171,6 +172,14 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { return ( + {/* + Mounted inside (which lives under per + convertLegacyAppRoot's children-recognition rules) so the + component's MutationObserver runs in the routed subtree. The + previous placement as an sibling was silently + dropped by convertLegacyAppRoot during the NFS migration. + */} + Skip to main content diff --git a/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx b/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx index c4af9a5ab..d0bf0185b 100644 --- a/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx +++ b/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx @@ -1,5 +1,6 @@ import { ScaffolderPage } from '@backstage/plugin-scaffolder'; import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react'; +import { ScaffolderLayout } from '../../scaffolder/ScaffolderLayout'; import { ComponentNamePickerFieldExtension } from '../../scaffolder/ComponentNamePicker'; import { ResourceNamePickerFieldExtension } from '../../scaffolder/ResourceNamePicker'; import { BuildTemplatePickerFieldExtension } from '../../scaffolder/BuildTemplatePicker'; @@ -41,17 +42,18 @@ import { CustomReviewStep } from '../../scaffolder/CustomReviewState'; */ export function OpenChoreoScaffolderPage() { return ( - + + @@ -82,6 +84,7 @@ export function OpenChoreoScaffolderPage() { - + + ); } From fac11c69b237fd748f2072a72e9d0440968225d3 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:32:40 +0530 Subject: [PATCH 32/44] fix(scaffolder): reset preselection state when query params absent Signed-off-by: Kavith Lokuhewage --- .../ScaffolderPreselectionContext.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/app/src/scaffolder/ScaffolderPreselectionContext.tsx b/packages/app/src/scaffolder/ScaffolderPreselectionContext.tsx index 717549360..16f2132d4 100644 --- a/packages/app/src/scaffolder/ScaffolderPreselectionContext.tsx +++ b/packages/app/src/scaffolder/ScaffolderPreselectionContext.tsx @@ -47,16 +47,15 @@ export const ScaffolderPreselectionProvider = ({ string | null >(null); - // Capture params on mount or when URL changes + // Capture params on mount or when URL changes. Reset to null when the + // param disappears so stale state doesn't leak across routes (the + // provider now lives at whole-app scope under Root). Without the + // explicit reset, visiting `?namespace=foo` anywhere in the app would + // seed a preselection that persisted into a later `/create` visit + // with no namespace query. useEffect(() => { - const projectParam = searchParams.get('project'); - if (projectParam) { - setPreselectedProject(projectParam); - } - const namespaceParam = searchParams.get('namespace'); - if (namespaceParam) { - setPreselectedNamespace(namespaceParam); - } + setPreselectedProject(searchParams.get('project')); + setPreselectedNamespace(searchParams.get('namespace')); }, [searchParams]); const clearPreselectedProject = () => { From f828a2b0764e20abcd5df8bc6aab8d0d42c87d89 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:43:00 +0530 Subject: [PATCH 33/44] chore(app): cleanup stale comments, duplicate kind icons, dead bindRoutes Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 41 +---------------- packages/app/src/apis.test.ts | 3 +- packages/app/src/apis.ts | 29 ------------ packages/app/src/apis/customOverrides.tsx | 33 +------------- .../app/src/components/DynamicSignInPage.tsx | 2 +- .../settings/OpenChoreoProviderSettings.tsx | 2 +- packages/app/src/kindIcons.ts | 44 +++++++++++++++++++ 7 files changed, 52 insertions(+), 102 deletions(-) create mode 100644 packages/app/src/kindIcons.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index a225bfc04..e4ad04ea8 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,5 +1,4 @@ import { Route } from 'react-router-dom'; -import { apiDocsPlugin } from '@backstage/plugin-api-docs'; import { CatalogEntityPage, CatalogIndexPage, @@ -41,7 +40,6 @@ import { DeploymentPipelineFormWithYamlFieldExtension } from './scaffolder/Deplo import { WorkloadDetailsFieldExtension } from './scaffolder/WorkloadDetailsField'; import { CustomTemplateListPage } from './components/scaffolder/CustomTemplateListPage'; import { CustomReviewStep } from './scaffolder/CustomReviewState'; -import { orgPlugin } from '@backstage/plugin-org'; import { SearchPage } from '@backstage/plugin-search'; import { TechDocsIndexPage, @@ -99,11 +97,7 @@ import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; import { appThemes } from './themes'; -import CloudIcon from '@material-ui/icons/Cloud'; -import DnsIcon from '@material-ui/icons/Dns'; -import AccountTreeIcon from '@material-ui/icons/AccountTree'; -import VisibilityIcon from '@material-ui/icons/Visibility'; -import BuildIcon from '@material-ui/icons/Build'; +import { LEGACY_KIND_ICONS } from './kindIcons'; import { AccessControlContent, SecretsContent, @@ -113,36 +107,11 @@ import { SettingsLayout, UserSettingsGeneral, } from '@backstage/plugin-user-settings'; -import CategoryIcon from '@material-ui/icons/Category'; -import LayersIcon from '@material-ui/icons/Layers'; -import StorageIcon from '@material-ui/icons/Storage'; -import ExtensionIcon from '@material-ui/icons/Extension'; -import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; -import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; import { VisitListener } from '@backstage/plugin-home'; const legacyAppOptions = convertLegacyAppOptions({ apis, - icons: { - 'kind:environment': CloudIcon, - 'kind:dataplane': DnsIcon, - 'kind:clusterdataplane': DnsIcon, - 'kind:deploymentpipeline': AccountTreeIcon, - 'kind:observabilityplane': VisibilityIcon, - 'kind:clusterobservabilityplane': VisibilityIcon, - 'kind:workflowplane': BuildIcon, - 'kind:clusterworkflowplane': BuildIcon, - 'kind:componenttype': CategoryIcon, - 'kind:clustercomponenttype': CategoryIcon, - 'kind:resourcetype': LayersIcon, - 'kind:clusterresourcetype': LayersIcon, - 'kind:resource': StorageIcon, - 'kind:traittype': ExtensionIcon, - 'kind:clustertraittype': ExtensionIcon, - 'kind:workflow': PlayCircleOutlineIcon, - 'kind:clusterworkflow': PlayCircleOutlineIcon, - 'kind:componentworkflow': SettingsApplicationsIcon, - }, + icons: LEGACY_KIND_ICONS, themes: appThemes, }); @@ -287,16 +256,10 @@ const app = createApp({ viewTechDoc: techdocsPlugin.routes.docRoot, createFromTemplate: scaffolderPlugin.routes.selectedTemplate, }); - bind(apiDocsPlugin.externalRoutes, { - registerApi: catalogImportPlugin.routes.importPage, - }); bind(scaffolderPlugin.externalRoutes, { registerComponent: catalogImportPlugin.routes.importPage, viewTechDoc: techdocsPlugin.routes.docRoot, }); - bind(orgPlugin.externalRoutes, { - catalogIndex: catalogPlugin.routes.catalogIndex, - }); }, }); diff --git a/packages/app/src/apis.test.ts b/packages/app/src/apis.test.ts index f1d69f192..00cc6911b 100644 --- a/packages/app/src/apis.test.ts +++ b/packages/app/src/apis.test.ts @@ -29,7 +29,8 @@ import { PerchAgentClient, } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; -import { apis, openChoreoAuthApiRef } from './apis'; +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. diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 0651c5021..2481e0f0a 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -25,7 +25,6 @@ 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 from separate file to avoid circular dependency with form decorators import { openChoreoAuthApiRef } from './apis/authRefs'; // NOTE: ``perchAgentApiRef`` is also declared on // ``openchoreoPerchPlugin.apis`` in plugins/openchoreo-portal-assistant/src/plugin.ts. @@ -39,8 +38,6 @@ import { perchAgentApiRef, PerchAgentClient, } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; -// Re-export for use by App.tsx and other components -export { openChoreoAuthApiRef }; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -127,27 +124,6 @@ export const apis: AnyApiFactory[] = [ factory: deps => UserSettingsStorage.create(deps), }), - // DEFERRED to Step 3c: Scaffolder form decorators that inject the user's - // OpenChoreo token as a secret (used by scaffolder actions for user-based - // authorization). Under NFS this collides with the scaffolder plugin's - // own default factory (API_FACTORY_CONFLICT). Will reinstate via - // scaffolderPlugin.withOverrides so it lives under pluginId `scaffolder`. - - // openChoreoCiClientApiRef and genericWorkflowsClientApiRef are now - // provided by their respective NFS plugins via `ApiBlueprint` (see - // plugins/openchoreo-ci/src/alpha.tsx and - // plugins/openchoreo-workflows/src/alpha.tsx). Registering them here - // would collide with the plugin-scoped factories under NFS. - - // DEFERRED to Step 3c: Catalog graph API override with custom OpenChoreo - // relations. The legacy `app`-scoped registration of `catalogGraphApiRef` - // collides with `@backstage/plugin-catalog-graph`'s own default factory - // under NFS (API_FACTORY_CONFLICT). The proper fix is a plugin override - // (catalogGraphPlugin.withOverrides) that disables the upstream default - // and provides our augmented one under the same pluginId. Until then, - // custom relations (deploysTo, hostedOn, instanceOf, …) won't render in - // entity Relations cards or the catalog graph. - // Assistant Agent client (Perch). Mirrors the registration on // openchoreoPerchPlugin.apis — see the import-site comment for why // both exist. @@ -161,9 +137,4 @@ export const apis: AnyApiFactory[] = [ new PerchAgentClient({ discoveryApi, fetchApi }), }), - // DEFERRED to Step 3c: Custom EntityPresentationApi with kind icons for - // Environment, DataPlane, DeploymentPipeline, etc. Same conflict shape as - // catalogGraphApiRef above — the override needs to come from a catalog - // plugin module so it sits under pluginId `catalog`, not `app`. Until - // then, those kinds get the default upstream icon in the catalog graph. ]; diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index ee291f6da..e1c513363 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -49,17 +49,7 @@ import { RELATION_BUILDS_ON, RELATION_BUILDS, } from '@openchoreo/backstage-plugin-common'; -import CloudIcon from '@material-ui/icons/Cloud'; -import DnsIcon from '@material-ui/icons/Dns'; -import AccountTreeIcon from '@material-ui/icons/AccountTree'; -import VisibilityIcon from '@material-ui/icons/Visibility'; -import BuildIcon from '@material-ui/icons/Build'; -import CategoryIcon from '@material-ui/icons/Category'; -import LayersIcon from '@material-ui/icons/Layers'; -import StorageIcon from '@material-ui/icons/Storage'; -import ExtensionIcon from '@material-ui/icons/Extension'; -import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; -import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; +import { KIND_ICONS } from '../kindIcons'; import { openChoreoTokenDecorator } from '../scaffolder/openChoreoTokenDecorator'; import { LogRowActionBlueprint } from '@openchoreo/backstage-plugin-openchoreo-observability/alpha'; import { InvestigateLogButton } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; @@ -148,26 +138,7 @@ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ factory: ({ catalogApi }) => DefaultEntityPresentationApi.create({ catalogApi, - kindIcons: { - environment: CloudIcon, - dataplane: DnsIcon, - clusterdataplane: DnsIcon, - deploymentpipeline: AccountTreeIcon, - observabilityplane: VisibilityIcon, - clusterobservabilityplane: VisibilityIcon, - workflowplane: BuildIcon, - clusterworkflowplane: BuildIcon, - componenttype: CategoryIcon, - clustercomponenttype: CategoryIcon, - resourcetype: LayersIcon, - clusterresourcetype: LayersIcon, - resource: StorageIcon, - traittype: ExtensionIcon, - clustertraittype: ExtensionIcon, - workflow: PlayCircleOutlineIcon, - clusterworkflow: PlayCircleOutlineIcon, - componentworkflow: SettingsApplicationsIcon, - }, + kindIcons: KIND_ICONS, }), }), }), diff --git a/packages/app/src/components/DynamicSignInPage.tsx b/packages/app/src/components/DynamicSignInPage.tsx index b74a97045..34984e443 100644 --- a/packages/app/src/components/DynamicSignInPage.tsx +++ b/packages/app/src/components/DynamicSignInPage.tsx @@ -1,7 +1,7 @@ import { SignInPage } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import type { SignInPageProps } from '@backstage/plugin-app-react'; -import { openChoreoAuthApiRef } from '../apis'; +import { openChoreoAuthApiRef } from '../apis/authRefs'; /** * Dynamic SignInPage that switches between OpenChoreo OIDC and guest mode diff --git a/packages/app/src/components/settings/OpenChoreoProviderSettings.tsx b/packages/app/src/components/settings/OpenChoreoProviderSettings.tsx index 5b8652bfe..075c1a406 100644 --- a/packages/app/src/components/settings/OpenChoreoProviderSettings.tsx +++ b/packages/app/src/components/settings/OpenChoreoProviderSettings.tsx @@ -1,6 +1,6 @@ import List from '@material-ui/core/List'; import { ProviderSettingsItem } from '@backstage/plugin-user-settings'; -import { openChoreoAuthApiRef } from '../../apis'; +import { openChoreoAuthApiRef } from '../../apis/authRefs'; import { OpenChoreoIcon } from '@openchoreo/backstage-design-system'; export const OpenChoreoProviderSettings = () => { diff --git a/packages/app/src/kindIcons.ts b/packages/app/src/kindIcons.ts new file mode 100644 index 000000000..5d417b1ab --- /dev/null +++ b/packages/app/src/kindIcons.ts @@ -0,0 +1,44 @@ +import type { IconComponent } from '@backstage/core-plugin-api'; +import CloudIcon from '@material-ui/icons/Cloud'; +import DnsIcon from '@material-ui/icons/Dns'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import BuildIcon from '@material-ui/icons/Build'; +import CategoryIcon from '@material-ui/icons/Category'; +import LayersIcon from '@material-ui/icons/Layers'; +import StorageIcon from '@material-ui/icons/Storage'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; +import SettingsApplicationsIcon from '@material-ui/icons/SettingsApplications'; + +/** + * Single source of truth for OpenChoreo platform kind icons. Both + * `App.tsx` (legacy `convertLegacyAppOptions.icons` shape, `kind:`) + * and `customOverrides.tsx` (NFS `DefaultEntityPresentationApi.kindIcons` + * shape, bare ``) derive their respective keyed shapes from this map. + */ +export const KIND_ICONS: Record = { + environment: CloudIcon, + dataplane: DnsIcon, + clusterdataplane: DnsIcon, + deploymentpipeline: AccountTreeIcon, + observabilityplane: VisibilityIcon, + clusterobservabilityplane: VisibilityIcon, + workflowplane: BuildIcon, + clusterworkflowplane: BuildIcon, + componenttype: CategoryIcon, + clustercomponenttype: CategoryIcon, + resourcetype: LayersIcon, + clusterresourcetype: LayersIcon, + resource: StorageIcon, + traittype: ExtensionIcon, + clustertraittype: ExtensionIcon, + workflow: PlayCircleOutlineIcon, + clusterworkflow: PlayCircleOutlineIcon, + componentworkflow: SettingsApplicationsIcon, +}; + +/** `kind:`-keyed shape consumed by `convertLegacyAppOptions.icons`. */ +export const LEGACY_KIND_ICONS = Object.fromEntries( + Object.entries(KIND_ICONS).map(([k, v]) => [`kind:${k}`, v]), +); From 4ce9acb2250027a526a74e53d9932675efeb5031 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Tue, 16 Jun 2026 15:47:18 +0530 Subject: [PATCH 34/44] chore: add changeset for NFS migration fixups Signed-off-by: Kavith Lokuhewage --- .changeset/fix-nfs-migration-followups.md | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .changeset/fix-nfs-migration-followups.md diff --git a/.changeset/fix-nfs-migration-followups.md b/.changeset/fix-nfs-migration-followups.md new file mode 100644 index 000000000..3fbf241a8 --- /dev/null +++ b/.changeset/fix-nfs-migration-followups.md @@ -0,0 +1,37 @@ +--- +'@openchoreo/backstage-plugin': patch +'@openchoreo/backstage-plugin-openchoreo-ci': patch +'@openchoreo/backstage-plugin-openchoreo-observability': patch +'@openchoreo/backstage-plugin-openchoreo-workflows': patch +'@openchoreo/backstage-plugin-react': patch +--- + +Follow-up fixes to the New Frontend System (NFS) migration. + +Custom catalog-graph relations, entity-presentation kind icons, and the +scaffolder form-decorator override are now actually applied at runtime — +the original NFS migration registered them but they were silently +overwritten by upstream defaults at startup. The form-decorator override +also stops dropping decorators contributed by other plugins. + +Entity tabs and overview cards that previously lived in the host's +`EntityPage.tsx` now ride through each plugin's `/alpha` export as +`EntityContentBlueprint` and `EntityCardBlueprint` extensions, with the +right kind filters. Adopters on `/alpha` get the full entity-page +contributions automatically: the OpenChoreo CI plugin contributes the +Build tab (scoped to `kind:component`); the observability plugin +contributes the 10 component- and system-page tabs (Logs, Events, +Metrics, Alerts, Wirelogs, Traces, Incidents, RCA Reports, Cost +Analysis) plus a registry API for host-injected log-row action renderers; +the OpenChoreo plugin contributes the Deploy tab, the system Cell +Diagram tab, the shared Resource Definition tab, and 30+ overview cards +spanning every OpenChoreo platform kind (Environment, DataPlane, +WorkflowPlane, ObservabilityPlane, DeploymentPipeline, the ComponentType +/ ResourceType / TraitType families, and the Workflow family); the +generic-workflows plugin contributes the Runs tab on `Workflow` and +`ClusterWorkflow` entities of type `Generic`. The react plugin exposes a +new `FeatureGatedContent` component so plugin authors can gate routable +extensions on the OpenChoreo feature flags without rolling their own +empty-state wrapper. + +Adopters still on the default (legacy) export are unaffected. From 0cb403dbb43c54694a67272c203d0b5a2c609e12 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 17 Jun 2026 06:02:27 +0530 Subject: [PATCH 35/44] chore: apply prettier to NFS migration files Signed-off-by: Kavith Lokuhewage --- packages/app/src/apis.ts | 1 - .../scaffolder/OpenChoreoScaffolderPage.tsx | 60 +++++++++---------- .../src/api/LogRowActionRendererApi.ts | 3 +- plugins/openchoreo/src/alpha.tsx | 4 +- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 2481e0f0a..3e504c057 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -136,5 +136,4 @@ export const apis: AnyApiFactory[] = [ factory: ({ discoveryApi, fetchApi }) => new PerchAgentClient({ discoveryApi, fetchApi }), }), - ]; diff --git a/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx b/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx index d0bf0185b..49ae12312 100644 --- a/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx +++ b/packages/app/src/components/scaffolder/OpenChoreoScaffolderPage.tsx @@ -54,36 +54,36 @@ export function OpenChoreoScaffolderPage() { ReviewStepComponent: CustomReviewStep, }} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts b/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts index 4b6c9c496..3affdc1b5 100644 --- a/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts +++ b/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.ts @@ -33,8 +33,7 @@ export class DefaultLogRowActionRendererApi implements LogRowActionRendererApi { } static create(options: { renderers: RenderLogRowAction[] }) { - const render: RenderLogRowAction = - options.renderers[0] ?? (() => null); + const render: RenderLogRowAction = options.renderers[0] ?? (() => null); return new DefaultLogRowActionRendererApi(render); } } diff --git a/plugins/openchoreo/src/alpha.tsx b/plugins/openchoreo/src/alpha.tsx index da35e0991..cca8647c6 100644 --- a/plugins/openchoreo/src/alpha.tsx +++ b/plugins/openchoreo/src/alpha.tsx @@ -122,7 +122,9 @@ const cellDiagramEntityContent = EntityContentBlueprint.make({ title: 'Cell Diagram', filter: 'kind:system', loader: () => - import('./components/CellDiagram/CellDiagram').then(m => ), + import('./components/CellDiagram/CellDiagram').then(m => ( + + )), }, }); From a79e3ab93ce334061f1a71a2f0d52fdfe8f4b0af Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 17 Jun 2026 06:17:59 +0530 Subject: [PATCH 36/44] test(app): cover customOverrides extension wiring Signed-off-by: Kavith Lokuhewage --- .../app/src/apis/customOverrides.test.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/app/src/apis/customOverrides.test.tsx diff --git a/packages/app/src/apis/customOverrides.test.tsx b/packages/app/src/apis/customOverrides.test.tsx new file mode 100644 index 000000000..26b82f1e3 --- /dev/null +++ b/packages/app/src/apis/customOverrides.test.tsx @@ -0,0 +1,47 @@ +import { + catalogGraphPluginAlpha, + catalogPluginAlpha, + customAppModule, + scaffolderPluginAlpha, +} from './customOverrides'; + +describe('customOverrides', () => { + it('exports a catalog-graph plugin override', () => { + expect(catalogGraphPluginAlpha).toBeDefined(); + expect((catalogGraphPluginAlpha as any).id).toBe('catalog-graph'); + }); + + it('exports a catalog plugin override', () => { + expect(catalogPluginAlpha).toBeDefined(); + expect((catalogPluginAlpha as any).id).toBe('catalog'); + }); + + it('exports a scaffolder plugin override', () => { + expect(scaffolderPluginAlpha).toBeDefined(); + expect((scaffolderPluginAlpha as any).id).toBe('scaffolder'); + }); + + it('exports the customAppModule frontend module', () => { + expect(customAppModule).toBeDefined(); + // `createFrontendModule({ pluginId: 'app', ... })` produces a frontend + // module bound to the `app` plugin id. + expect( + (customAppModule as any).id ?? (customAppModule as any).pluginId, + ).toBe('app'); + }); + + it('registers extensions on the customAppModule (SignInPage, Translation, LogRowAction, host cards)', () => { + const extensions = ((customAppModule as any).extensions ?? []) as Array<{ + id: string; + }>; + expect(Array.isArray(extensions)).toBe(true); + expect(extensions.length).toBeGreaterThan(0); + + const ids = extensions.map(e => e.id); + // The host registers exactly five extensions on the app module today: + // a SignInPage, a Translation override (catalog-import), a + // LogRowAction renderer, the OpenChoreoAboutCard, and the + // WorkflowsOrExternalCICard. + expect(ids).toHaveLength(5); + }); +}); From 80bd4aecaa20a0e50f90cb633a2655c99e50e03c Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 17 Jun 2026 06:17:59 +0530 Subject: [PATCH 37/44] test(openchoreo-observability): cover LogRowActionRendererApi and alpha extensions Signed-off-by: Kavith Lokuhewage --- .../src/alpha.test.tsx | 50 +++++++++++++++++++ .../src/api/LogRowActionRendererApi.test.ts | 43 ++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 plugins/openchoreo-observability/src/alpha.test.tsx create mode 100644 plugins/openchoreo-observability/src/api/LogRowActionRendererApi.test.ts diff --git a/plugins/openchoreo-observability/src/alpha.test.tsx b/plugins/openchoreo-observability/src/alpha.test.tsx new file mode 100644 index 000000000..d8b390d23 --- /dev/null +++ b/plugins/openchoreo-observability/src/alpha.test.tsx @@ -0,0 +1,50 @@ +import observabilityPlugin, { + LogRowActionBlueprint, + logRowActionRendererApiRef, +} from './alpha'; + +describe('openchoreo-observability alpha plugin', () => { + it('registers under the openchoreo-observability plugin id', () => { + expect((observabilityPlugin as any).id).toBe('openchoreo-observability'); + }); + + it('re-exports the LogRowActionBlueprint and renderer api ref', () => { + expect(LogRowActionBlueprint).toBeDefined(); + expect(LogRowActionBlueprint.dataRefs.renderer).toBeDefined(); + expect(logRowActionRendererApiRef.id).toBe( + 'plugin.openchoreo-observability.log-row-action-renderer', + ); + }); + + it('exposes the expected blueprint extensions', () => { + const extensions = (observabilityPlugin as any).extensions as Array<{ + id: string; + }>; + expect(Array.isArray(extensions)).toBe(true); + + const ids = extensions.map(e => e.id); + const plugin = 'openchoreo-observability'; + for (const expected of [ + // backend client apis + `api:${plugin}/observability`, + `api:${plugin}/rca-agent`, + `api:${plugin}/finops-agent`, + // host-injection registry + `api:${plugin}/log-row-action-renderer`, + // component-page entity tabs + `entity-content:${plugin}/runtime-logs`, + `entity-content:${plugin}/runtime-events`, + `entity-content:${plugin}/metrics`, + `entity-content:${plugin}/alerts`, + `entity-content:${plugin}/wirelogs`, + // system-page entity tabs + `entity-content:${plugin}/project-runtime-logs`, + `entity-content:${plugin}/traces`, + `entity-content:${plugin}/project-incidents`, + `entity-content:${plugin}/rca-reports`, + `entity-content:${plugin}/cost-analysis`, + ]) { + expect(ids).toContain(expected); + } + }); +}); diff --git a/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.test.ts b/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.test.ts new file mode 100644 index 000000000..9e6a322f4 --- /dev/null +++ b/plugins/openchoreo-observability/src/api/LogRowActionRendererApi.test.ts @@ -0,0 +1,43 @@ +import { + DefaultLogRowActionRendererApi, + logRowActionRendererApiRef, +} from './LogRowActionRendererApi'; + +describe('logRowActionRendererApiRef', () => { + it('uses the canonical id under the openchoreo-observability plugin', () => { + expect(logRowActionRendererApiRef.id).toBe( + 'plugin.openchoreo-observability.log-row-action-renderer', + ); + }); +}); + +describe('DefaultLogRowActionRendererApi', () => { + it('exposes the first renderer when one is contributed', () => { + const sentinel = 'rendered-action' as any; + const renderer = jest.fn().mockReturnValue(sentinel); + + const api = DefaultLogRowActionRendererApi.create({ + renderers: [renderer], + }); + + const fakeLog = { id: '1' } as any; + const getSnapshot = jest.fn(); + expect(api.render(fakeLog, getSnapshot)).toBe(sentinel); + expect(renderer).toHaveBeenCalledWith(fakeLog, getSnapshot); + }); + + it('falls back to a no-op renderer when no renderers are contributed', () => { + const api = DefaultLogRowActionRendererApi.create({ renderers: [] }); + expect(api.render({} as any, jest.fn())).toBeNull(); + }); + + it('picks the first renderer when multiple are contributed', () => { + const first = jest.fn().mockReturnValue('first'); + const second = jest.fn().mockReturnValue('second'); + const api = DefaultLogRowActionRendererApi.create({ + renderers: [first as any, second as any], + }); + expect(api.render({} as any, jest.fn())).toBe('first'); + expect(second).not.toHaveBeenCalled(); + }); +}); From 60d9a24b8e580080aa6b1c882ece8b995f723a36 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 17 Jun 2026 06:17:59 +0530 Subject: [PATCH 38/44] test(openchoreo): cover alpha extension registrations Signed-off-by: Kavith Lokuhewage --- plugins/openchoreo/src/alpha.test.tsx | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 plugins/openchoreo/src/alpha.test.tsx diff --git a/plugins/openchoreo/src/alpha.test.tsx b/plugins/openchoreo/src/alpha.test.tsx new file mode 100644 index 000000000..1534d356d --- /dev/null +++ b/plugins/openchoreo/src/alpha.test.tsx @@ -0,0 +1,77 @@ +import openchoreoPlugin from './alpha'; + +const ALPHA_EXTENSION_NAMES = [ + // backend client + ['api', 'open-choreo-client'], + // shared + ['entity-content', 'resource-definition'], + // component-page + ['entity-content', 'component-deploy'], + ['entity-card', 'deployment-status'], + ['entity-card', 'runtime-health'], + // system-page + ['entity-content', 'cell-diagram'], + ['entity-card', 'project-contents'], + ['entity-card', 'deployment-pipeline'], + // domain-page + ['entity-card', 'namespace-projects'], + ['entity-card', 'namespace-resources'], + // managed resource + ['entity-content', 'resource-deploy'], + ['entity-card', 'resource-parameters'], + ['entity-card', 'resource-deployments'], + ['entity-card', 'consuming-components'], + // environment + ['entity-card', 'environment-status-summary'], + ['entity-card', 'environment-promotion'], + ['entity-card', 'environment-deployed-components'], + ['entity-card', 'environment-gateway-configuration'], + // dataplane + ['entity-card', 'dataplane-status'], + ['entity-card', 'dataplane-environments'], + ['entity-card', 'dataplane-gateway-configuration'], + ['entity-card', 'cluster-dataplane-status'], + ['entity-card', 'cluster-dataplane-environments'], + ['entity-card', 'cluster-dataplane-gateway-configuration'], + // workflow plane + ['entity-card', 'workflow-plane-status'], + ['entity-card', 'cluster-workflow-plane-status'], + // observability plane + ['entity-card', 'observability-plane-status'], + ['entity-card', 'observability-plane-linked-planes'], + ['entity-card', 'cluster-observability-plane-status'], + ['entity-card', 'cluster-observability-plane-linked-planes'], + // deployment pipeline + ['entity-card', 'deployment-pipeline-visualization'], + ['entity-card', 'promotion-paths'], + // type families + ['entity-card', 'component-type-overview'], + ['entity-card', 'resource-type-overview'], + ['entity-card', 'trait-type-overview'], + // workflow family + ['entity-card', 'workflow-overview'], + ['entity-card', 'component-workflow-overview'], +] as const; + +describe('openchoreo alpha plugin', () => { + it('registers under the openchoreo plugin id', () => { + expect((openchoreoPlugin as any).id).toBe('openchoreo'); + }); + + it('exposes the documented blueprint extensions', () => { + const extensions = (openchoreoPlugin as any).extensions as Array<{ + id: string; + }>; + expect(Array.isArray(extensions)).toBe(true); + + const ids = extensions.map(e => e.id); + for (const [kind, name] of ALPHA_EXTENSION_NAMES) { + expect(ids).toContain(`${kind}:openchoreo/${name}`); + } + }); + + it('exposes one extension per documented entry (no silent drops)', () => { + const extensions = (openchoreoPlugin as any).extensions as Array; + expect(extensions).toHaveLength(ALPHA_EXTENSION_NAMES.length); + }); +}); From 933b02ff703fa871e96d85e1f4ac804bf533af34 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 17 Jun 2026 06:17:59 +0530 Subject: [PATCH 39/44] test(openchoreo-workflows): cover Workflow Runs alpha extension Signed-off-by: Kavith Lokuhewage --- .../openchoreo-workflows/src/alpha.test.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 plugins/openchoreo-workflows/src/alpha.test.tsx diff --git a/plugins/openchoreo-workflows/src/alpha.test.tsx b/plugins/openchoreo-workflows/src/alpha.test.tsx new file mode 100644 index 000000000..2947a3131 --- /dev/null +++ b/plugins/openchoreo-workflows/src/alpha.test.tsx @@ -0,0 +1,24 @@ +import workflowsPlugin from './alpha'; + +describe('openchoreo-workflows alpha plugin', () => { + it('registers under the openchoreo-workflows plugin id', () => { + expect((workflowsPlugin as any).id).toBe('openchoreo-workflows'); + }); + + it('exposes the expected blueprint extensions', () => { + const extensions = (workflowsPlugin as any).extensions as Array<{ + id: string; + }>; + expect(Array.isArray(extensions)).toBe(true); + + const ids = extensions.map(e => e.id); + const plugin = 'openchoreo-workflows'; + for (const expected of [ + `api:${plugin}/generic-workflows-client`, + `page:${plugin}/generic-workflows`, + `entity-content:${plugin}/workflow-runs`, + ]) { + expect(ids).toContain(expected); + } + }); +}); From 239bb69418dd88ddc53b4aad87c1ac75c0278abc Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 17 Jun 2026 15:24:12 +0530 Subject: [PATCH 40/44] fix(app): restore custom entity page via page:catalog/entity override The createApp feature reorder let the NFS page:catalog/entity extension shadow the legacy CatalogEntityPage mount, dropping the custom CompactEntityHeader, hand-authored Overview JSX, and OpenChoreoEntityLayout tab styles. Override the NFS loader to render entityPage inside OpenChoreoCatalogEntityPage and remove the now-unused legacy route mount and host-side overview EntityCardBlueprints. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 20 ++--- .../app/src/apis/customOverrides.test.tsx | 15 ++-- packages/app/src/apis/customOverrides.tsx | 87 +++++++------------ .../catalog/OpenChoreoCatalogEntityPage.tsx | 80 +++++++++++++++++ 4 files changed, 128 insertions(+), 74 deletions(-) create mode 100644 packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index e4ad04ea8..9271a0230 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,9 +1,5 @@ import { Route } from 'react-router-dom'; -import { - CatalogEntityPage, - CatalogIndexPage, - catalogPlugin, -} from '@backstage/plugin-catalog'; +import { CatalogIndexPage, catalogPlugin } from '@backstage/plugin-catalog'; import { CatalogImportPage, catalogImportPlugin, @@ -49,7 +45,6 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { apis } from './apis'; -import { entityPage } from './components/catalog/EntityPage'; import { CustomCatalogPage } from './components/catalog/CustomCatalogPage'; import { CustomApiExplorerPage } from './components/catalog/CustomApiExplorerPage'; import { searchPage } from './components/search/SearchPage'; @@ -121,12 +116,13 @@ const routes = ( }> - } - > - {entityPage} - + {/* + The entity route (`/catalog/:namespace/:kind/:name`) is owned by the + NFS `page:catalog/entity` extension — see customOverrides.tsx where + we override its loader to wrap the legacy `entityPage` JSX in + `OpenChoreoCatalogEntityPage` so the custom header, tab styles, and + hand-authored per-kind Overview layouts are preserved. + */} } /> { ).toBe('app'); }); - it('registers extensions on the customAppModule (SignInPage, Translation, LogRowAction, host cards)', () => { + it('registers extensions on the customAppModule (SignInPage, Translation, LogRowAction)', () => { const extensions = ((customAppModule as any).extensions ?? []) as Array<{ id: string; }>; expect(Array.isArray(extensions)).toBe(true); expect(extensions.length).toBeGreaterThan(0); - const ids = extensions.map(e => e.id); - // The host registers exactly five extensions on the app module today: - // a SignInPage, a Translation override (catalog-import), a - // LogRowAction renderer, the OpenChoreoAboutCard, and the - // WorkflowsOrExternalCICard. - expect(ids).toHaveLength(5); + // The host registers exactly three extensions on the app module today: + // a SignInPage, a Translation override (catalog-import), and a + // LogRowAction renderer. Overview-slot cards (OpenChoreoAboutCard, + // WorkflowsOrExternalCICard) used to live here but moved back into the + // hand-authored `entityPage` JSX when we restored the custom + // page:catalog/entity override — see customOverrides.tsx for context. + expect(extensions).toHaveLength(3); }); }); diff --git a/packages/app/src/apis/customOverrides.tsx b/packages/app/src/apis/customOverrides.tsx index e1c513363..2ce49dd70 100644 --- a/packages/app/src/apis/customOverrides.tsx +++ b/packages/app/src/apis/customOverrides.tsx @@ -53,8 +53,6 @@ import { KIND_ICONS } from '../kindIcons'; import { openChoreoTokenDecorator } from '../scaffolder/openChoreoTokenDecorator'; import { LogRowActionBlueprint } from '@openchoreo/backstage-plugin-openchoreo-observability/alpha'; import { InvestigateLogButton } from '@openchoreo/backstage-plugin-openchoreo-portal-assistant'; -import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; -import type { Entity } from '@backstage/catalog-model'; /** * Override `catalog-graph`'s default `api:catalog-graph` to include the @@ -117,6 +115,22 @@ export const catalogGraphPluginAlpha = * landed, the legacy `` mount won; under the * reorder, upstream's NFS extension wins by default — so we override its * loader explicitly. + * + * Finally, overrides `page:catalog/entity` so the entity page rides through + * our `OpenChoreoCatalogEntityPage` (which sets up `AsyncEntityProvider` + + * `EntityLayoutWithDelete` wrapping `OpenChoreoEntityLayout` with the + * dropdown-driven `CompactEntityHeader` and styled tab bar). The hand- + * authored per-kind layouts in `entityPage` (Overview Grid, custom + * EntityCatalogGraphCard, FailedBuildSnackbar, etc.) are rendered as + * `` children — `OpenChoreoEntityLayout` accepts the + * 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. */ export const catalogPluginAlpha = catalogPluginAlphaBase.withOverrides({ extensions: [ @@ -142,6 +156,22 @@ 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} + + ); + }, + }, + }), ], }); @@ -205,59 +235,6 @@ export const customAppModule = createFrontendModule({ ), }, }), - // OpenChoreoAboutCard sits in the Overview "info" slot on every - // OpenChoreo-relevant entity kind. Lives in `packages/app` (not in - // the openchoreo plugin) because the About card is the host's - // composition of upstream's About metadata + OpenChoreo-specific - // edit affordances — a host concern, not a plugin one. - EntityCardBlueprint.make({ - name: 'openchoreo-about', - params: { - type: 'info', - filter: (entity: Entity) => - [ - 'component', - 'system', - 'domain', - 'resource', - 'environment', - 'dataplane', - 'clusterdataplane', - 'workflowplane', - 'clusterworkflowplane', - 'observabilityplane', - 'clusterobservabilityplane', - 'deploymentpipeline', - 'componenttype', - 'resourcetype', - 'clustercomponenttype', - 'clusterresourcetype', - 'traittype', - 'clustertraittype', - 'workflow', - 'clusterworkflow', - 'componentworkflow', - ].includes(entity.kind.toLowerCase()), - loader: () => - import('../components/catalog/OpenChoreoAboutCard').then(m => ( - - )), - }, - }), - // CI status card — internally branches between the OpenChoreo - // WorkflowsOverviewCard and an external-CI card (Jenkins / GitHub - // Actions / GitLab) based on the entity's CI annotation. Component - // pages only. - EntityCardBlueprint.make({ - name: 'workflows-or-external-ci', - params: { - filter: 'kind:component', - loader: () => - import('../components/catalog/WorkflowsOrExternalCICard').then(m => ( - - )), - }, - }), ], }); diff --git a/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx new file mode 100644 index 000000000..a58fd2f5b --- /dev/null +++ b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx @@ -0,0 +1,80 @@ +import type { ReactNode } from 'react'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useAsyncRetry from 'react-use/esm/useAsyncRetry'; +import type { Entity } from '@backstage/catalog-model'; +import { + errorApiRef, + useApi, + useRouteRefParams, +} from '@backstage/core-plugin-api'; +import { + AsyncEntityProvider, + catalogApiRef, + entityRouteRef, + type EntityLoadingStatus, +} from '@backstage/plugin-catalog-react'; +import { EntityLayoutWithDelete } from './EntityLayoutWithDelete'; + +/** + * Local copy of upstream's internal `useEntityFromUrl` hook (not part of + * `@backstage/plugin-catalog`'s public surface — see + * `node_modules/@backstage/plugin-catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts`). + * Inlined here so the NFS `page:catalog/entity` override can mount its own + * `AsyncEntityProvider` without relying on a private import. + */ +function useEntityFromUrl(): EntityLoadingStatus { + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); + const navigate = useNavigate(); + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { + value: entity, + error, + loading, + retry: refresh, + } = useAsyncRetry( + () => + catalogApi.getEntityByRef({ kind, namespace, name }) as Promise< + Entity | undefined + >, + [catalogApi, kind, namespace, name], + ); + + useEffect(() => { + if (!name) { + errorApi.post(new Error('No name provided!')); + navigate('/'); + } + }, [errorApi, navigate, error, loading, entity, name]); + + return { entity, loading, error, refresh }; +} + +interface OpenChoreoCatalogEntityPageProps { + /** + * Layout children — the legacy `entityPage` `` plus any + * dynamic NFS-contributed `` entries from + * `inputs.contents`. + */ + children: ReactNode; +} + +/** + * Replacement for the legacy `` mount, used by the NFS + * `page:catalog/entity` extension override in `customOverrides.tsx`. Owns + * the `AsyncEntityProvider` (so descendant `useEntity()` calls work) and + * delegates layout to `EntityLayoutWithDelete`, which wraps + * `OpenChoreoEntityLayout` with the OpenChoreo header, delete + + * edit-annotations menu items, and existence-check empty states. + */ +export function OpenChoreoCatalogEntityPage({ + children, +}: OpenChoreoCatalogEntityPageProps) { + return ( + + {children} + + ); +} From 1dde17e85482f4b5158783827793015645a43f5d Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Thu, 18 Jun 2026 13:37:48 +0530 Subject: [PATCH 41/44] fix: unblock entity page render under page:catalog/entity override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate EntityLayoutWithDelete on useAsyncEntity, remove the double wrap in OpenChoreoCatalogEntityPage (per-kind pages already provide it), and swap observability entity-tab exports to React.lazy — the unbound rootRouteRef crashed every observability tab. Signed-off-by: Kavith Lokuhewage --- .../catalog/EntityLayoutWithDelete.tsx | 62 ++++++-- .../catalog/OpenChoreoCatalogEntityPage.tsx | 20 +-- .../openchoreo-observability/src/plugin.ts | 140 +++++++----------- 3 files changed, 114 insertions(+), 108 deletions(-) diff --git a/packages/app/src/components/catalog/EntityLayoutWithDelete.tsx b/packages/app/src/components/catalog/EntityLayoutWithDelete.tsx index 02fbd8364..85db8bff9 100644 --- a/packages/app/src/components/catalog/EntityLayoutWithDelete.tsx +++ b/packages/app/src/components/catalog/EntityLayoutWithDelete.tsx @@ -1,6 +1,6 @@ import { type ReactNode } from 'react'; import { Box } from '@material-ui/core'; -import { useEntity } from '@backstage/plugin-catalog-react'; +import { useAsyncEntity, useEntity } from '@backstage/plugin-catalog-react'; import { EmptyState, Progress } from '@backstage/core-components'; import { VisuallyHidden } from '@openchoreo/backstage-design-system'; import { @@ -59,15 +59,15 @@ interface EntityLayoutWithDeleteProps { } /** - * Wrapper component that adds delete menu functionality to OpenChoreoEntityLayout. - * Children (OpenChoreoEntityLayout.Route elements) are passed through, keeping them - * in static JSX so Backstage can discover routable extensions. - * - * Also checks if the entity exists in OpenChoreo: - * - If not found (404), shows empty state with "Not Found" message - * - If marked for deletion, shows empty state with "Marked for Deletion" message + * Inner content component that does the actual rendering once the entity is + * known to be loaded. Called by the gating `EntityLayoutWithDelete` wrapper + * below — must NEVER be rendered when `useAsyncEntity()` is still loading, + * because `useEntity()` throws on undefined entity and the four custom hooks + * (`useResourceDefinitionPermission`, `useDeleteEntityMenuItems`, + * `useAnnotationEditorMenuItems`, `useEntityExistsCheck`) all access + * `entity.kind` / `entity.metadata` unconditionally. */ -export function EntityLayoutWithDelete({ +function EntityLayoutWithDeleteContent({ children, kindDisplayNames, parentEntityRelations = ['partOf'], @@ -194,3 +194,47 @@ export function EntityLayoutWithDelete({ ); } + +/** + * Gating wrapper. Lives directly under `AsyncEntityProvider` (see + * `OpenChoreoCatalogEntityPage`) and handles the loading / error / missing + * states upstream `EntityLayout` would normally handle. Once the entity is + * loaded, defers to `EntityLayoutWithDeleteContent` for the real rendering. + * + * Necessary because `EntityLayoutWithDeleteContent` calls `useEntity()` and + * several entity-dependent hooks unconditionally — calling them during the + * loading window throws (`useEntity` rejects undefined entity). + */ +export function EntityLayoutWithDelete(props: EntityLayoutWithDeleteProps) { + const { entity, loading, error } = useAsyncEntity(); + + if (loading) { + return ; + } + + if (error) { + return ( + + + + ); + } + + if (!entity) { + return ( + + + + ); + } + + return ; +} diff --git a/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx index a58fd2f5b..ec865d702 100644 --- a/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx +++ b/packages/app/src/components/catalog/OpenChoreoCatalogEntityPage.tsx @@ -14,7 +14,6 @@ import { entityRouteRef, type EntityLoadingStatus, } from '@backstage/plugin-catalog-react'; -import { EntityLayoutWithDelete } from './EntityLayoutWithDelete'; /** * Local copy of upstream's internal `useEntityFromUrl` hook (not part of @@ -54,9 +53,12 @@ function useEntityFromUrl(): EntityLoadingStatus { interface OpenChoreoCatalogEntityPageProps { /** - * Layout children — the legacy `entityPage` `` plus any - * dynamic NFS-contributed `` entries from - * `inputs.contents`. + * The legacy `entityPage` ``. Each per-kind branch + * (`componentPage`, `dataplanePage`, etc.) already opens with its own + * `` + `` children, + * so we only need to provide the `AsyncEntityProvider` here — wrapping + * the switch in another `EntityLayoutWithDelete` would double-wrap and + * fail `OpenChoreoEntityLayout`'s strict Route-child check. */ children: ReactNode; } @@ -64,17 +66,17 @@ interface OpenChoreoCatalogEntityPageProps { /** * Replacement for the legacy `` mount, used by the NFS * `page:catalog/entity` extension override in `customOverrides.tsx`. Owns - * the `AsyncEntityProvider` (so descendant `useEntity()` calls work) and - * delegates layout to `EntityLayoutWithDelete`, which wraps - * `OpenChoreoEntityLayout` with the OpenChoreo header, delete + - * edit-annotations menu items, and existence-check empty states. + * the `AsyncEntityProvider` so descendant `useAsyncEntity()` / + * `useEntity()` calls in the per-kind layouts work; layout/header/tab + * styling lives inside each per-kind page's own `EntityLayoutWithDelete` → + * `OpenChoreoEntityLayout`. */ export function OpenChoreoCatalogEntityPage({ children, }: OpenChoreoCatalogEntityPageProps) { return ( - {children} + {children} ); } diff --git a/plugins/openchoreo-observability/src/plugin.ts b/plugins/openchoreo-observability/src/plugin.ts index 7fee0734a..1dc5c901f 100644 --- a/plugins/openchoreo-observability/src/plugin.ts +++ b/plugins/openchoreo-observability/src/plugin.ts @@ -1,6 +1,6 @@ +import { lazy } from 'react'; import { createPlugin, - createRoutableExtension, createApiFactory, discoveryApiRef, fetchApiRef, @@ -50,109 +50,69 @@ export const openchoreoObservabilityPlugin = createPlugin({ ], }); -export const ObservabilityMetrics = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityMetrics', - component: () => - import('./components/Metrics/ObservabilityMetricsPage').then( - m => m.ObservabilityMetricsPage, - ), - mountPoint: rootRouteRef, - }), +/** + * Entity-page tab components. Previously wrapped in + * `createRoutableExtension({ mountPoint: rootRouteRef })`, but `rootRouteRef` + * is never bound to a real mounted path — these are tab content, not + * standalone pages, so the routable wrapper would throw + * "Routable extension component was not discovered in the app element tree" + * at render time. Exported as `React.lazy` components so the page bundle + * still code-splits. + */ +export const ObservabilityMetrics = lazy(() => + import('./components/Metrics/ObservabilityMetricsPage').then(m => ({ + default: m.ObservabilityMetricsPage, + })), ); -export const ObservabilityTraces = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityTraces', - component: () => - import('./components/Traces/ObservabilityTracesPage').then( - m => m.ObservabilityTracesPage, - ), - mountPoint: rootRouteRef, - }), +export const ObservabilityTraces = lazy(() => + import('./components/Traces/ObservabilityTracesPage').then(m => ({ + default: m.ObservabilityTracesPage, + })), ); -export const ObservabilityRCA = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityRCA', - component: () => import('./components/RCA/RCAPage').then(m => m.RCAPage), - mountPoint: rootRouteRef, - }), +export const ObservabilityRCA = lazy(() => + import('./components/RCA/RCAPage').then(m => ({ default: m.RCAPage })), ); -export const ObservabilityRuntimeLogs = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityRuntimeLogs', - component: () => - import('./components/RuntimeLogs/ObservabilityRuntimeLogsPage').then( - m => m.ObservabilityRuntimeLogsPage, - ), - mountPoint: rootRouteRef, - }), +export const ObservabilityRuntimeLogs = lazy(() => + import('./components/RuntimeLogs/ObservabilityRuntimeLogsPage').then(m => ({ + default: m.ObservabilityRuntimeLogsPage, + })), ); -export const ObservabilityRuntimeEvents = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityRuntimeEvents', - component: () => - import('./components/RuntimeEvents/ObservabilityRuntimeEventsPage').then( - m => m.ObservabilityRuntimeEventsPage, - ), - mountPoint: rootRouteRef, - }), +export const ObservabilityRuntimeEvents = lazy(() => + import('./components/RuntimeEvents/ObservabilityRuntimeEventsPage').then( + m => ({ default: m.ObservabilityRuntimeEventsPage }), + ), ); -export const ObservabilityProjectRuntimeLogs = - openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityProjectRuntimeLogs', - component: () => - import( - './components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage' - ).then(m => m.ObservabilityProjectRuntimeLogsPage), - mountPoint: rootRouteRef, - }), - ); +export const ObservabilityProjectRuntimeLogs = lazy(() => + import('./components/RuntimeLogs/ObservabilityProjectRuntimeLogsPage').then( + m => ({ default: m.ObservabilityProjectRuntimeLogsPage }), + ), +); -export const ObservabilityAlerts = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityAlerts', - component: () => - import('./components/Alerts/ObservabilityAlertsPage').then( - m => m.ObservabilityAlertsPage, - ), - mountPoint: rootRouteRef, - }), +export const ObservabilityAlerts = lazy(() => + import('./components/Alerts/ObservabilityAlertsPage').then(m => ({ + default: m.ObservabilityAlertsPage, + })), ); -export const ObservabilityWirelogs = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityWirelogs', - component: () => - import('./components/Wirelogs/ObservabilityWirelogsPage').then( - m => m.ObservabilityWirelogsPage, - ), - mountPoint: rootRouteRef, - }), +export const ObservabilityWirelogs = lazy(() => + import('./components/Wirelogs/ObservabilityWirelogsPage').then(m => ({ + default: m.ObservabilityWirelogsPage, + })), ); -export const ObservabilityProjectIncidents = - openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityProjectIncidents', - component: () => - import('./components/Incidents/ObservabilityProjectIncidentsPage').then( - m => m.ObservabilityProjectIncidentsPage, - ), - mountPoint: rootRouteRef, - }), - ); +export const ObservabilityProjectIncidents = lazy(() => + import('./components/Incidents/ObservabilityProjectIncidentsPage').then( + m => ({ default: m.ObservabilityProjectIncidentsPage }), + ), +); -export const ObservabilityCostAnalysis = openchoreoObservabilityPlugin.provide( - createRoutableExtension({ - name: 'ObservabilityCostAnalysis', - component: () => - import('./components/CostAnalysis').then(m => m.CostAnalysisPage), - mountPoint: rootRouteRef, - }), +export const ObservabilityCostAnalysis = lazy(() => + import('./components/CostAnalysis').then(m => ({ + default: m.CostAnalysisPage, + })), ); From 9713b2f1b89453badbf6d24c5892672bf67ed514 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Thu, 18 Jun 2026 14:52:49 +0530 Subject: [PATCH 42/44] fix(app): register apiDocs and kubernetes NFS plugins Without their /alpha features in createApp, apiDocsConfigRef, kubernetesApiRef, and the related kubernetes apis are missing from the api holder. The Definition tab on kind:api entities and the Kubernetes tab on annotated components throw NotImplementedError at render time. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9271a0230..fa4edf630 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -88,6 +88,13 @@ import { // resolves under NFS. Our legacy `` // mount in `` provides the actual page rendering. import catalogImportPluginAlpha from '@backstage/plugin-catalog-import/alpha'; +// api-docs and kubernetes NFS plugins — registered so that `apiDocsConfigRef`, +// `kubernetesApiRef`, etc. are present in the api holder. The host owns the +// `/api-docs` route (CustomApiExplorerPage) and the Kubernetes entity tab +// reuses upstream `EntityKubernetesContent`; without these features the apis +// they depend on are absent and the tabs throw `NotImplementedError`. +import apiDocsPluginAlpha from '@backstage/plugin-api-docs/alpha'; +import kubernetesPluginAlpha from '@backstage/plugin-kubernetes/alpha'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; @@ -240,6 +247,8 @@ const app = createApp({ catalogGraphPluginAlpha, catalogPluginAlpha, catalogImportPluginAlpha, + apiDocsPluginAlpha, + kubernetesPluginAlpha, openchoreoPluginAlpha, openchoreoCiPluginAlpha, openchoreoObservabilityPluginAlpha, From 7b07aa183efd3b0ac69bc9672b9c38eca7c3e2e1 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Thu, 18 Jun 2026 14:56:32 +0530 Subject: [PATCH 43/44] fix: merge changelogs for the PR Signed-off-by: Kavith Lokuhewage --- .changeset/fix-nfs-migration-followups.md | 37 ----------------------- .changeset/migrate-portal-to-nfs.md | 9 ++++-- 2 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 .changeset/fix-nfs-migration-followups.md diff --git a/.changeset/fix-nfs-migration-followups.md b/.changeset/fix-nfs-migration-followups.md deleted file mode 100644 index 3fbf241a8..000000000 --- a/.changeset/fix-nfs-migration-followups.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@openchoreo/backstage-plugin': patch -'@openchoreo/backstage-plugin-openchoreo-ci': patch -'@openchoreo/backstage-plugin-openchoreo-observability': patch -'@openchoreo/backstage-plugin-openchoreo-workflows': patch -'@openchoreo/backstage-plugin-react': patch ---- - -Follow-up fixes to the New Frontend System (NFS) migration. - -Custom catalog-graph relations, entity-presentation kind icons, and the -scaffolder form-decorator override are now actually applied at runtime — -the original NFS migration registered them but they were silently -overwritten by upstream defaults at startup. The form-decorator override -also stops dropping decorators contributed by other plugins. - -Entity tabs and overview cards that previously lived in the host's -`EntityPage.tsx` now ride through each plugin's `/alpha` export as -`EntityContentBlueprint` and `EntityCardBlueprint` extensions, with the -right kind filters. Adopters on `/alpha` get the full entity-page -contributions automatically: the OpenChoreo CI plugin contributes the -Build tab (scoped to `kind:component`); the observability plugin -contributes the 10 component- and system-page tabs (Logs, Events, -Metrics, Alerts, Wirelogs, Traces, Incidents, RCA Reports, Cost -Analysis) plus a registry API for host-injected log-row action renderers; -the OpenChoreo plugin contributes the Deploy tab, the system Cell -Diagram tab, the shared Resource Definition tab, and 30+ overview cards -spanning every OpenChoreo platform kind (Environment, DataPlane, -WorkflowPlane, ObservabilityPlane, DeploymentPipeline, the ComponentType -/ ResourceType / TraitType families, and the Workflow family); the -generic-workflows plugin contributes the Runs tab on `Workflow` and -`ClusterWorkflow` entities of type `Generic`. The react plugin exposes a -new `FeatureGatedContent` component so plugin authors can gate routable -extensions on the OpenChoreo feature flags without rolling their own -empty-state wrapper. - -Adopters still on the default (legacy) export are unaffected. diff --git a/.changeset/migrate-portal-to-nfs.md b/.changeset/migrate-portal-to-nfs.md index effe99259..20d67c794 100644 --- a/.changeset/migrate-portal-to-nfs.md +++ b/.changeset/migrate-portal-to-nfs.md @@ -4,10 +4,15 @@ '@openchoreo/backstage-plugin-openchoreo-observability': patch '@openchoreo/backstage-plugin-openchoreo-workflows': patch '@openchoreo/backstage-plugin-platform-engineer-core': patch +'@openchoreo/backstage-plugin-react': patch --- Add an `/alpha` entry point that exposes each plugin as a `createFrontendPlugin` for use with Backstage's New Frontend System (NFS). The default entry continues to export the legacy `createPlugin` instance so existing host apps keep working unchanged; adopters on NFS can now import `from '@openchoreo/backstage-plugin-/alpha'` and include the plugin directly in `createApp({ features: [...] })`. -The `/alpha` exports register each plugin's API factories (e.g. `openChoreoCiClientApiRef`, `genericWorkflowsClientApiRef`, the three observability backend clients, `openChoreoClientApiRef`) and one top-level page where applicable (`platform-engineer-core`'s dashboard view, `openchoreo-workflows`' generic workflows page, `openchoreo-ci`'s workflows entity tab). Entity tabs and component cards that the host app mounts with per-call props (e.g. ``) remain on the legacy export for now; a future release will move those host-injected callables behind a registry API so they can ride through NFS extensions too. +The `/alpha` exports register each plugin's API factories (e.g. `openChoreoCiClientApiRef`, `genericWorkflowsClientApiRef`, the three observability backend clients, `openChoreoClientApiRef`) and one top-level page where applicable (`platform-engineer-core`'s dashboard view, `openchoreo-workflows`' generic workflows page, `openchoreo-ci`'s workflows entity tab). -This addresses the body of [openchoreo/openchoreo#3568](https://github.com/openchoreo/openchoreo/issues/3568) — adopters can drop `--legacy` from the `@backstage/create-app` step when installing the plugin suite into an existing Backstage host. +Entity tabs and overview cards that previously lived in the host's `EntityPage.tsx` now ride through each plugin's `/alpha` export as `EntityContentBlueprint` and `EntityCardBlueprint` extensions, with the right kind filters. Adopters on `/alpha` get the full entity-page contributions automatically: the OpenChoreo CI plugin contributes the Build tab (scoped to `kind:component`); the observability plugin contributes the 10 component- and system-page tabs (Logs, Events, Metrics, Alerts, Wirelogs, Traces, Incidents, RCA Reports, Cost Analysis) plus a registry API for host-injected log-row action renderers; the OpenChoreo plugin contributes the Deploy tab, the system Cell Diagram tab, the shared Resource Definition tab, and 30+ overview cards spanning every OpenChoreo platform kind (Environment, DataPlane, WorkflowPlane, ObservabilityPlane, DeploymentPipeline, the ComponentType / ResourceType / TraitType families, and the Workflow family); the generic-workflows plugin contributes the Runs tab on `Workflow` and `ClusterWorkflow` entities of type `Generic`. The react plugin exposes a new `FeatureGatedContent` component so plugin authors can gate routable extensions on the OpenChoreo feature flags without rolling their own empty-state wrapper. + +Custom catalog-graph relations, entity-presentation kind icons, and the scaffolder form-decorator override are now actually applied at runtime — the original migration registered them but they were silently overwritten by upstream defaults at startup. The form-decorator override also stops dropping decorators contributed by other plugins. + +Adopters still on the default (legacy) export are unaffected. This addresses the body of [openchoreo/openchoreo#3568](https://github.com/openchoreo/openchoreo/issues/3568) — adopters can drop `--legacy` from the `@backstage/create-app` step when installing the plugin suite into an existing Backstage host. From b1deafe5ca2e9b9f86cac86b9e1645c2fbedff55 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Thu, 18 Jun 2026 15:55:43 +0530 Subject: [PATCH 44/44] fix(app): drop redundant page header on /catalog and /create Both pages rendered the small "Catalog" / "Create" title link above the real h1. The host's CustomCatalogPage and OpenChoreoScaffolderPage each mount their own PageWithHeader, while the NFS PageBlueprint's PageLayout emits another. Pass noHeader: true on the page:catalog and page:scaffolder overrides, and delete the now-redundant legacy and mounts (and their 27 field-extension imports) from App.tsx. Signed-off-by: Kavith Lokuhewage --- packages/app/src/App.tsx | 107 ++++------------------ packages/app/src/apis/customOverrides.tsx | 10 ++ 2 files changed, 28 insertions(+), 89 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index fa4edf630..eebae1c56 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -1,41 +1,10 @@ import { Route } from 'react-router-dom'; -import { CatalogIndexPage, catalogPlugin } from '@backstage/plugin-catalog'; +import { catalogPlugin } from '@backstage/plugin-catalog'; import { CatalogImportPage, catalogImportPlugin, } from '@backstage/plugin-catalog-import'; -import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; -import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react'; -import { ComponentNamePickerFieldExtension } from './scaffolder/ComponentNamePicker'; -import { ResourceNamePickerFieldExtension } from './scaffolder/ResourceNamePicker'; -import { BuildTemplatePickerFieldExtension } from './scaffolder/BuildTemplatePicker'; -import { BuildTemplateParametersFieldExtension } from './scaffolder/BuildTemplateParameters'; -import { BuildWorkflowPickerFieldExtension } from './scaffolder/BuildWorkflowPicker'; -import { BuildWorkflowParametersFieldExtension } from './scaffolder/BuildWorkflowParameters'; -import { TraitsFieldExtension } from './scaffolder/TraitsField'; -import { SwitchFieldExtension } from './scaffolder/SwitchField'; -import { AdvancedConfigurationFieldExtension } from './scaffolder/AdvancedConfigurationField'; -import { DeploymentSourcePickerFieldExtension } from './scaffolder/DeploymentSourcePicker'; -import { BuildAndDeployFieldExtension } from './scaffolder/BuildAndDeployField'; -import { ContainerImageFieldExtension } from './scaffolder/ContainerImageField'; -import { ComponentTypeYamlEditorFieldExtension } from './scaffolder/ComponentTypeYamlEditor'; -import { TraitYamlEditorFieldExtension } from './scaffolder/TraitYamlEditor'; -import { ClusterComponentTypeYamlEditorFieldExtension } from './scaffolder/ClusterComponentTypeYamlEditor'; -import { ClusterResourceTypeYamlEditorFieldExtension } from './scaffolder/ClusterResourceTypeYamlEditor'; -import { ResourceTypeYamlEditorFieldExtension } from './scaffolder/ResourceTypeYamlEditor'; -import { ResourceParametersFieldExtension } from './scaffolder/ResourceParametersField'; -import { ClusterTraitYamlEditorFieldExtension } from './scaffolder/ClusterTraitYamlEditor'; -import { ComponentWorkflowYamlEditorFieldExtension } from './scaffolder/ComponentWorkflowYamlEditor'; -import { ClusterWorkflowYamlEditorFieldExtension } from './scaffolder/ClusterWorkflowYamlEditor'; -import { GitSourceFieldExtension } from './scaffolder/GitSourceField'; -import { ProjectNamespaceFieldExtension } from './scaffolder/ProjectNamespaceField'; -import { NamespaceEntityPickerFieldExtension } from './scaffolder/NamespaceEntityPicker'; -import { DeploymentPipelinePickerFieldExtension } from './scaffolder/DeploymentPipelinePicker'; -import { EnvironmentFormWithYamlFieldExtension } from './scaffolder/EnvironmentFormWithYaml'; -import { DeploymentPipelineFormWithYamlFieldExtension } from './scaffolder/DeploymentPipelineFormWithYaml'; -import { WorkloadDetailsFieldExtension } from './scaffolder/WorkloadDetailsField'; -import { CustomTemplateListPage } from './components/scaffolder/CustomTemplateListPage'; -import { CustomReviewStep } from './scaffolder/CustomReviewState'; +import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechDocsIndexPage, @@ -45,7 +14,6 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; import { apis } from './apis'; -import { CustomCatalogPage } from './components/catalog/CustomCatalogPage'; import { CustomApiExplorerPage } from './components/catalog/CustomApiExplorerPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; @@ -120,15 +88,14 @@ const legacyAppOptions = convertLegacyAppOptions({ const routes = ( } /> - }> - - {/* - The entity route (`/catalog/:namespace/:kind/:name`) is owned by the - NFS `page:catalog/entity` extension — see customOverrides.tsx where - we override its loader to wrap the legacy `entityPage` JSX in - `OpenChoreoCatalogEntityPage` so the custom header, tab styles, and - hand-authored per-kind Overview layouts are preserved. + `/catalog` is owned by the NFS `page:catalog` extension and + `/catalog/:namespace/:kind/:name` by `page:catalog/entity` — see + customOverrides.tsx, which overrides each loader to render the + host's `` and the legacy `entityPage` JSX + respectively. The legacy `` mount used to + live here but double-rendered the catalog header under the NFS + compat shim. */} } /> - - } - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {/* + `/create` is owned by the NFS `page:scaffolder` extension — see + customOverrides.tsx, which overrides its loader to render + `` (the host's `` with + the 27 field-extension children and `CustomTemplateListPage` / + `CustomReviewStep` components). The legacy `` + mount used to live here but double-rendered the scaffolder header + under the NFS compat shim. + */} } /> `; without this we render two + // page headers, one above the other. + noHeader: true, loader: () => import('../components/catalog/CustomCatalogPage').then(m => ( @@ -248,6 +253,11 @@ export const scaffolderPluginAlpha = scaffolderPluginAlphaBase.withOverrides({ // every host customization. scaffolderPluginAlphaBase.getExtension('page:scaffolder').override({ params: { + // Same `noHeader: true` reason as the `page:catalog` override above + // — `OpenChoreoScaffolderPage` mounts a `` that + // renders its own page header. + noHeader: true, loader: () => import('../components/scaffolder/OpenChoreoScaffolderPage').then( m => ,