From 1ade9cef48dd46c8b1b39d4f1d35e65739da2322 Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 12 Aug 2026 17:55:24 +0200 Subject: [PATCH 1/6] IS-11630 Add the devops-dashboard plugin with the DevOps Dashboard page and Database Clients sub-page. --- .../packages/app/package.json | 1 + .../plugins/devops-dashboard/.eslintrc.js | 1 + .../plugins/devops-dashboard/README.md | 66 +++++ .../plugins/devops-dashboard/dev/index.tsx | 5 + .../plugins/devops-dashboard/package.json | 52 ++++ .../DbClientsSection/DbClientsSection.tsx | 11 + .../src/components/DbClientsSection/index.ts | 1 + .../plugins/devops-dashboard/src/index.ts | 1 + .../devops-dashboard/src/plugin.test.ts | 7 + .../plugins/devops-dashboard/src/plugin.tsx | 53 ++++ .../plugins/devops-dashboard/src/routes.ts | 4 + .../devops-dashboard/src/setupTests.ts | 1 + src/devops-dashboard-backstage-app/yarn.lock | 242 +++++++++++++++++- 13 files changed, 441 insertions(+), 4 deletions(-) create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/.eslintrc.js create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/dev/index.tsx create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/package.json create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/index.ts create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/index.ts create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.test.ts create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.tsx create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/routes.ts create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/setupTests.ts diff --git a/src/devops-dashboard-backstage-app/packages/app/package.json b/src/devops-dashboard-backstage-app/packages/app/package.json index 6cb78dd2..3b9d85ab 100644 --- a/src/devops-dashboard-backstage-app/packages/app/package.json +++ b/src/devops-dashboard-backstage-app/packages/app/package.json @@ -38,6 +38,7 @@ "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.38", "@backstage/plugin-user-settings": "^0.9.5", "@backstage/ui": "^0.17.0", + "@internal/backstage-plugin-devops-dashboard": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "common": "workspace:^", diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/.eslintrc.js b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/.eslintrc.js new file mode 100644 index 00000000..e2a53a6a --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md new file mode 100644 index 00000000..a96e6197 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md @@ -0,0 +1,66 @@ +# DevOps Dashboard plugin + +A Backstage plugin for the Curity DevOps Dashboard. + +## What is the Curity DevOps Dashboard? + +The [Curity Identity Server](https://curity.io) separates configuration from +runtime data. Server configuration belongs to administrators and lives in the +admin UI. Runtime data — such as OAuth clients stored in a database, sessions, +or tokens — changes while the server runs, and the people who need to see it +are usually operations and support teams, not administrators. + +The DevOps Dashboard is the view for those teams. It shows the runtime data of +a running Curity Identity Server without giving access to server +configuration. + +This plugin brings the DevOps Dashboard into [Backstage](https://backstage.io), +so teams can use it inside the developer portal they already work in. It adds +a **DevOps Dashboard** entry to the Backstage sidebar, with one tab per +section. + +## Requirements + +- A Backstage app built on the + [frontend system](https://backstage.io/docs/frontend-system/architecture/index). +- A running Curity Identity Server, reachable from the user's browser. +- Users must be able to authenticate against the Curity Identity Server, and + their access token must carry the admin API scope + (`urn:se:curity:scopes:admin:api`). The app can sign users in through + Curity directly, or keep its own sign-in and add Curity as an extra auth + provider. Users without the scope see an access-denied message. + +## Install the plugin in your own Backstage app + +Installation follows the standard Backstage steps: + +1. Add the package to your app: + + ```sh + yarn --cwd packages/app add @internal/backstage-plugin-devops-dashboard + ``` + +2. Let your app discover it. If your `app-config.yaml` enables package + discovery, you are done: + + ```yaml + app: + packages: all + ``` + + Without discovery, register the plugin explicitly in your app's + `createApp` call: + + ```tsx + import devopsDashboardPlugin from '@internal/backstage-plugin-devops-dashboard'; + + const app = createApp({ + features: [devopsDashboardPlugin], + }); + ``` + +3. Configure sign-in through Curity as described under + [Requirements](#requirements). + +The **DevOps Dashboard** entry then appears in the sidebar, and the page is +available at `/devops-dashboard`. diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/dev/index.tsx b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/dev/index.tsx new file mode 100644 index 00000000..21f84292 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/dev/index.tsx @@ -0,0 +1,5 @@ +import { createDevApp } from '@backstage/frontend-dev-utils'; + +import plugin from '../src'; + +createDevApp({ features: [plugin] }); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/package.json b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/package.json new file mode 100644 index 00000000..55e4e564 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/package.json @@ -0,0 +1,52 @@ +{ + "name": "@internal/backstage-plugin-devops-dashboard", + "version": "0.1.0", + "license": "UNLICENSED", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin", + "pluginId": "devops-dashboard" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.18.12", + "@backstage/frontend-plugin-api": "^0.17.3", + "@backstage/theme": "^0.7.3", + "@backstage/ui": "^0.17.0", + "@material-ui/icons": "^4.9.1", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.36.4", + "@backstage/frontend-defaults": "^0.5.4", + "@backstage/frontend-dev-utils": "^0.1.4", + "@backstage/frontend-test-utils": "^0.6.2", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.0.0", + "msw": "^1.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx new file mode 100644 index 00000000..3382449c --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx @@ -0,0 +1,11 @@ +/** + * Static placeholder for the Database Clients section. The real content + * (table, search, states) lands in the next step; this only proves the + * plugin structure: sidebar entry → page → tab → section. + */ +export const DbClientsSection = () => ( +
+

Database Clients

+

Static section placeholder — content arrives in the next commit.

+
+); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/index.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/index.ts new file mode 100644 index 00000000..a8539df2 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/index.ts @@ -0,0 +1 @@ +export { DbClientsSection } from './DbClientsSection'; diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/index.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/index.ts new file mode 100644 index 00000000..e48bdc4d --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/index.ts @@ -0,0 +1 @@ +export { devopsDashboardPlugin as default } from './plugin'; diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.test.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.test.ts new file mode 100644 index 00000000..04a19e2e --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { devopsDashboardPlugin } from './plugin'; + +describe('devops-dashboard', () => { + it('should export plugin', () => { + expect(devopsDashboardPlugin).toBeDefined(); + }); +}); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.tsx b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.tsx new file mode 100644 index 00000000..d05c05eb --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/plugin.tsx @@ -0,0 +1,53 @@ +import { + createFrontendPlugin, + PageBlueprint, + SubPageBlueprint, +} from '@backstage/frontend-plugin-api'; +import StorageIcon from '@material-ui/icons/Storage'; +import DashboardIcon from '@material-ui/icons/Dashboard'; + +import { dbClientsRouteRef, rootRouteRef } from './routes'; + +export const PLUGIN_ID = 'devops-dashboard'; + +/** + * Extension IDs follow `:[/]`, where the namespace + * defaults to the plugin ID. Sub-pages attach to the page through this. + */ +const PAGE_EXTENSION_ID = `page:${PLUGIN_ID}`; + +/** + * The DevOps Dashboard page. Its title and icon double as the app's + * sidebar entry (nav items are derived from page extensions). + * Sections attach as sub-pages and render as tabs in the page header. + */ +export const homePage = PageBlueprint.make({ + params: { + path: '/devops-dashboard', + title: 'DevOps Dashboard', + icon: , + routeRef: rootRouteRef, + }, +}); + +export const dbClientsSubPage = SubPageBlueprint.make({ + attachTo: { id: PAGE_EXTENSION_ID, input: 'pages' }, + name: 'db-clients', + params: { + path: 'db-clients', + title: 'Database Clients', + icon: , + routeRef: dbClientsRouteRef, + loader: () => + import('./components/DbClientsSection').then(m => ), + }, +}); + +export const devopsDashboardPlugin = createFrontendPlugin({ + pluginId: PLUGIN_ID, + extensions: [homePage, dbClientsSubPage], + routes: { + root: rootRouteRef, + dbClients: dbClientsRouteRef, + }, +}); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/routes.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/routes.ts new file mode 100644 index 00000000..53dac644 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/routes.ts @@ -0,0 +1,4 @@ +import { createRouteRef } from '@backstage/frontend-plugin-api'; + +export const rootRouteRef = createRouteRef(); +export const dbClientsRouteRef = createRouteRef(); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/setupTests.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/setupTests.ts new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/src/devops-dashboard-backstage-app/yarn.lock b/src/devops-dashboard-backstage-app/yarn.lock index cd88019e..dc04653c 100644 --- a/src/devops-dashboard-backstage-app/yarn.lock +++ b/src/devops-dashboard-backstage-app/yarn.lock @@ -2118,6 +2118,26 @@ __metadata: languageName: node linkType: hard +"@backstage/frontend-dev-utils@npm:^0.1.4": + version: 0.1.4 + resolution: "@backstage/frontend-dev-utils@npm:0.1.4" + dependencies: + "@backstage/frontend-defaults": "npm:^0.5.4" + "@backstage/frontend-plugin-api": "npm:^0.17.3" + "@backstage/plugin-app": "npm:^0.5.1" + "@backstage/ui": "npm:^0.17.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/3bb6163003ddfff00b5ee8fbbc2550ab3700ce50bd5d183d92b6c2787d4dc995df5486dd59369fb397be0f3694a4659b34dfe8213b9905f267224d2c01ebebcf + languageName: node + linkType: hard + "@backstage/frontend-plugin-api@npm:^0.17.3": version: 0.17.3 resolution: "@backstage/frontend-plugin-api@npm:0.17.3" @@ -5087,6 +5107,30 @@ __metadata: languageName: node linkType: hard +"@internal/backstage-plugin-devops-dashboard@workspace:^, @internal/backstage-plugin-devops-dashboard@workspace:plugins/devops-dashboard": + version: 0.0.0-use.local + resolution: "@internal/backstage-plugin-devops-dashboard@workspace:plugins/devops-dashboard" + dependencies: + "@backstage/cli": "npm:^0.36.4" + "@backstage/core-components": "npm:^0.18.12" + "@backstage/frontend-defaults": "npm:^0.5.4" + "@backstage/frontend-dev-utils": "npm:^0.1.4" + "@backstage/frontend-plugin-api": "npm:^0.17.3" + "@backstage/frontend-test-utils": "npm:^0.6.2" + "@backstage/theme": "npm:^0.7.3" + "@backstage/ui": "npm:^0.17.0" + "@material-ui/icons": "npm:^4.9.1" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^14.0.0" + "@testing-library/user-event": "npm:^14.0.0" + msw: "npm:^1.0.0" + react: "npm:^16.13.1 || ^17.0.0 || ^18.0.0" + react-use: "npm:^17.2.4" + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + languageName: unknown + linkType: soft + "@internationalized/date@npm:^3.12.0, @internationalized/date@npm:^3.12.1, @internationalized/date@npm:^3.12.2": version: 3.12.2 resolution: "@internationalized/date@npm:3.12.2" @@ -6659,6 +6703,32 @@ __metadata: languageName: node linkType: hard +"@mswjs/cookies@npm:^0.2.2": + version: 0.2.2 + resolution: "@mswjs/cookies@npm:0.2.2" + dependencies: + "@types/set-cookie-parser": "npm:^2.4.0" + set-cookie-parser: "npm:^2.4.6" + checksum: 10c0/f950062538d431674d581309cf19884fc4d3f57e2a276164cac0c9a3250071d42464ba7825d13be14c703ca5a912d62a62626f4a068d8f36d1629dbb63bde740 + languageName: node + linkType: hard + +"@mswjs/interceptors@npm:^0.17.10": + version: 0.17.10 + resolution: "@mswjs/interceptors@npm:0.17.10" + dependencies: + "@open-draft/until": "npm:^1.0.3" + "@types/debug": "npm:^4.1.7" + "@xmldom/xmldom": "npm:^0.8.3" + debug: "npm:^4.3.3" + headers-polyfill: "npm:3.2.5" + outvariant: "npm:^1.2.1" + strict-event-emitter: "npm:^0.2.4" + web-encoding: "npm:^1.1.5" + checksum: 10c0/0343a93711b60c321c40733d6bf2720a736d8e0730f5d0d9916ee4a24abfcfca4a83d1e4b2e21c3affef4fc61f04588104be002fbc8258dc4b0d202c384ade33 + languageName: node + linkType: hard + "@mui/core-downloads-tracker@npm:^5.18.0": version: 5.18.0 resolution: "@mui/core-downloads-tracker@npm:5.18.0" @@ -7466,6 +7536,13 @@ __metadata: languageName: node linkType: hard +"@open-draft/until@npm:^1.0.3": + version: 1.0.3 + resolution: "@open-draft/until@npm:1.0.3" + checksum: 10c0/f88bcd774b55359d14a4fa80f7bfe7d9d6d26a5995e94e823e43b211656daae3663e983f0a996937da286d22f6f5da2087b661845302f236ba27f8529dcd14fb + languageName: node + linkType: hard + "@openapi-contrib/openapi-schema-to-json-schema@npm:~3.2.0": version: 3.2.0 resolution: "@openapi-contrib/openapi-schema-to-json-schema@npm:3.2.0" @@ -10607,6 +10684,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/cookie@npm:0.4.1" + checksum: 10c0/f96afe12bd51be1ec61410b0641243d93fa3a494702407c787a4c872b5c8bcd39b224471452055e44a9ce42af1a636e87d161994226eaf4c2be9c30f60418409 + languageName: node + linkType: hard + "@types/cors@npm:^2.8.6": version: 2.8.19 resolution: "@types/cors@npm:2.8.19" @@ -10616,7 +10700,7 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.0.0": +"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.7": version: 4.1.13 resolution: "@types/debug@npm:4.1.13" dependencies: @@ -10821,6 +10905,13 @@ __metadata: languageName: node linkType: hard +"@types/js-levenshtein@npm:^1.1.1": + version: 1.1.3 + resolution: "@types/js-levenshtein@npm:1.1.3" + checksum: 10c0/025f2bd8d865cfa7a996799a1a2f2a77fa2fc74a28971aa035a103de35d7c1e3d949721a88f57fdb532815bbcb2bf7019196a608ed0a8bbd1023d64c52bb251b + languageName: node + linkType: hard + "@types/js-yaml@npm:^4.0.1": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" @@ -11193,6 +11284,15 @@ __metadata: languageName: node linkType: hard +"@types/set-cookie-parser@npm:^2.4.0": + version: 2.4.10 + resolution: "@types/set-cookie-parser@npm:2.4.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/010b0c582ea70a2088618b4725808e80c30cce296c19ec58e51d94e0fd1038201b7b99238bf3ea74e1894163c8037d10a4f1729de62b2801ce240ff070f43e76 + languageName: node + linkType: hard + "@types/sockjs@npm:^0.3.36": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -11891,6 +11991,13 @@ __metadata: languageName: node linkType: hard +"@xmldom/xmldom@npm:^0.8.3": + version: 0.8.13 + resolution: "@xmldom/xmldom@npm:0.8.13" + checksum: 10c0/06405ee6fffba631abf715a305ace338420ebcea8baf1317f19f2752f5c505952b7df45159908e7be8451a42faa54326b780616ab4d08242b20477b2973da24b + languageName: node + linkType: hard + "@xobotyi/scrollbar-width@npm:^1.9.5": version: 1.9.5 resolution: "@xobotyi/scrollbar-width@npm:1.9.5" @@ -11950,6 +12057,13 @@ __metadata: languageName: node linkType: hard +"@zxing/text-encoding@npm:0.9.0": + version: 0.9.0 + resolution: "@zxing/text-encoding@npm:0.9.0" + checksum: 10c0/d15bff181d46c2ab709e7242801a8d40408aa8c19b44462e5f60e766bf59105b44957914ab6baab60d10d466a5e965f21fe890c67dfdb7d5c7f940df457b4d0d + languageName: node + linkType: hard + "a-sync-waterfall@npm:^1.0.0": version: 1.0.1 resolution: "a-sync-waterfall@npm:1.0.1" @@ -12345,6 +12459,7 @@ __metadata: "@backstage/plugin-techdocs-module-addons-contrib": "npm:^1.1.38" "@backstage/plugin-user-settings": "npm:^0.9.5" "@backstage/ui": "npm:^0.17.0" + "@internal/backstage-plugin-devops-dashboard": "workspace:^" "@material-ui/core": "npm:^4.12.2" "@material-ui/icons": "npm:^4.9.1" "@playwright/test": "npm:^1.32.3" @@ -14545,6 +14660,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.4.2": + version: 0.4.2 + resolution: "cookie@npm:0.4.2" + checksum: 10c0/beab41fbd7c20175e3a2799ba948c1dcc71ef69f23fe14eeeff59fc09f50c517b0f77098db87dbb4c55da802f9d86ee86cdc1cd3efd87760341551838d53fca2 + languageName: node + linkType: hard + "copy-to-clipboard@npm:^3.2.0, copy-to-clipboard@npm:^3.3.1, copy-to-clipboard@npm:^3.3.3": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -18284,7 +18406,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^16.0.0": +"graphql@npm:^16.0.0, graphql@npm:^16.8.1": version: 16.14.2 resolution: "graphql@npm:16.14.2" checksum: 10c0/a95a96961eaff55cc9fe9d31fae6f33499ac988b972d07ea5085024cb1333f515b902f376e7393a5489aa82200a8aff3eb96580e4d1b69d702ed19b6eb1ce97a @@ -18575,6 +18697,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:3.2.5": + version: 3.2.5 + resolution: "headers-polyfill@npm:3.2.5" + checksum: 10c0/10202f4ebfaecd6aa31305f29664f876ac01d9174a3fb8fcc5a0df3eaf9c1767fb0d6cf6f961484f2bfd2101b6768090976f146bd88aeedd07af4e741cb2dcb7 + languageName: node + linkType: hard + "helmet@npm:^6.0.0": version: 6.2.0 resolution: "helmet@npm:6.2.0" @@ -19611,6 +19740,13 @@ __metadata: languageName: node linkType: hard +"is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 10c0/5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed + languageName: node + linkType: hard + "is-number-object@npm:^1.1.1": version: 1.1.1 resolution: "is-number-object@npm:1.1.1" @@ -20667,6 +20803,13 @@ __metadata: languageName: node linkType: hard +"js-levenshtein@npm:^1.1.6": + version: 1.1.6 + resolution: "js-levenshtein@npm:1.1.6" + checksum: 10c0/14045735325ea1fd87f434a74b11d8a14380f090f154747e613529c7cff68b5ee607f5230fa40665d5fb6125a3791f4c223f73b9feca754f989b059f5c05864f + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -23009,6 +23152,40 @@ __metadata: languageName: node linkType: hard +"msw@npm:^1.0.0": + version: 1.3.5 + resolution: "msw@npm:1.3.5" + dependencies: + "@mswjs/cookies": "npm:^0.2.2" + "@mswjs/interceptors": "npm:^0.17.10" + "@open-draft/until": "npm:^1.0.3" + "@types/cookie": "npm:^0.4.1" + "@types/js-levenshtein": "npm:^1.1.1" + chalk: "npm:^4.1.1" + chokidar: "npm:^3.4.2" + cookie: "npm:^0.4.2" + graphql: "npm:^16.8.1" + headers-polyfill: "npm:3.2.5" + inquirer: "npm:^8.2.0" + is-node-process: "npm:^1.2.0" + js-levenshtein: "npm:^1.1.6" + node-fetch: "npm:^2.6.7" + outvariant: "npm:^1.4.0" + path-to-regexp: "npm:^6.3.0" + strict-event-emitter: "npm:^0.4.3" + type-fest: "npm:^2.19.0" + yargs: "npm:^17.3.1" + peerDependencies: + typescript: ">= 4.4.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 10c0/bb0b3625b68f1750bfe90ade6e9e98c64f509138ce7b09c8a53af19f8f662ac79881dad64fbc74c9426247b725e3ec5e6d45eea2f6b71ddc02184e0ddf743e4d + languageName: node + linkType: hard + "multer@npm:^2.0.2": version: 2.2.0 resolution: "multer@npm:2.2.0" @@ -23925,6 +24102,13 @@ __metadata: languageName: node linkType: hard +"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0": + version: 1.4.3 + resolution: "outvariant@npm:1.4.3" + checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c + languageName: node + linkType: hard + "own-keys@npm:^1.0.1": version: 1.0.2 resolution: "own-keys@npm:1.0.2" @@ -24347,6 +24531,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 + languageName: node + linkType: hard + "path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.3.0": version: 8.4.2 resolution: "path-to-regexp@npm:8.4.2" @@ -26454,7 +26645,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.0.2": +"react@npm:^16.13.1 || ^17.0.0 || ^18.0.0, react@npm:^18.0.2": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -27608,6 +27799,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.4.6": + version: 2.7.2 + resolution: "set-cookie-parser@npm:2.7.2" + checksum: 10c0/4381a9eb7ee951dfe393fe7aacf76b9a3b4e93a684d2162ab35594fa4053cc82a4d7d7582bf397718012c9adcf839b8cd8f57c6c42901ea9effe33c752da4a45 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -28227,6 +28425,22 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.2.4": + version: 0.2.8 + resolution: "strict-event-emitter@npm:0.2.8" + dependencies: + events: "npm:^3.3.0" + checksum: 10c0/6891e19fea4f0289e4da2fe7050d85906eaca7f774aa38fe674f0e58fdece1b63b868614fa23974c4cb862aa99358caa987523b705fdfff4639231c62e384394 + languageName: node + linkType: hard + +"strict-event-emitter@npm:^0.4.3": + version: 0.4.6 + resolution: "strict-event-emitter@npm:0.4.6" + checksum: 10c0/d0231ef081cb1937b1445da59a1ec202d1c097d825c504f398600532490a4104e200b0dce4137467a8eaac5f8f9718d01c99869687afad78cad3b14c4b2e6a39 + languageName: node + linkType: hard + "string-hash@npm:^1.1.1": version: 1.1.3 resolution: "string-hash@npm:1.1.3" @@ -29495,6 +29709,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^2.19.0": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb + languageName: node + linkType: hard + "type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" @@ -30378,6 +30599,19 @@ __metadata: languageName: node linkType: hard +"web-encoding@npm:^1.1.5": + version: 1.1.5 + resolution: "web-encoding@npm:1.1.5" + dependencies: + "@zxing/text-encoding": "npm:0.9.0" + util: "npm:^0.12.3" + dependenciesMeta: + "@zxing/text-encoding": + optional: true + checksum: 10c0/59d5413338ec0894c690006f5d8508b0c88cae1d8c78606c3f326e351c672196461ed808b849fe08d0900fa56a61fcacb9ff576499068d2ead0a7bc04afa7d34 + languageName: node + linkType: hard + "web-namespaces@npm:^2.0.0": version: 2.0.1 resolution: "web-namespaces@npm:2.0.1" @@ -30940,7 +31174,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.7.2": +"yargs@npm:^17.3.1, yargs@npm:^17.7.2": version: 17.7.3 resolution: "yargs@npm:17.7.3" dependencies: From 02d8223a4499fe9e72ad507cb972ae0527ab6a0c Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 12 Aug 2026 18:34:57 +0200 Subject: [PATCH 2/6] IS-11630 Add e2e coverage for the Curity sign-in gate and the DevOps Dashboard page. --- .../e2e-test-utils.ts | 20 ++++++++++++++++ .../packages/app/e2e-tests/app.test.ts | 8 +++---- .../playwright.config.ts | 6 ++++- .../devops-dashboard/e2e-tests/plugin.test.ts | 23 +++++++++++++++++++ 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 src/devops-dashboard-backstage-app/e2e-test-utils.ts create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts diff --git a/src/devops-dashboard-backstage-app/e2e-test-utils.ts b/src/devops-dashboard-backstage-app/e2e-test-utils.ts new file mode 100644 index 00000000..62b85f72 --- /dev/null +++ b/src/devops-dashboard-backstage-app/e2e-test-utils.ts @@ -0,0 +1,20 @@ +import { Page } from '@playwright/test'; + +/** + * The app signs in through Curity automatically on load (SignInPage `auto`), + * so the OAuth popup opens without any click. The dev server's test + * authenticator normally completes on its own and the popup closes itself; + * if the login app asks to pick a user, choose janedoe. + */ +export async function signInToCurity(page: Page): Promise { + const popup = await page.waitForEvent('popup'); + try { + await popup.waitForEvent('close', { timeout: 15_000 }); + } catch { + await popup + .getByText(/janedoe/i) + .first() + .click(); + await popup.waitForEvent('close'); + } +} diff --git a/src/devops-dashboard-backstage-app/packages/app/e2e-tests/app.test.ts b/src/devops-dashboard-backstage-app/packages/app/e2e-tests/app.test.ts index 9ef68bbe..a4572b30 100644 --- a/src/devops-dashboard-backstage-app/packages/app/e2e-tests/app.test.ts +++ b/src/devops-dashboard-backstage-app/packages/app/e2e-tests/app.test.ts @@ -15,13 +15,11 @@ */ import { test, expect } from '@playwright/test'; +import { signInToCurity } from '../../../e2e-test-utils'; -test('App should render the welcome page', async ({ page }) => { +test('App should render after signing in through Curity', async ({ page }) => { await page.goto('/'); - - const enterButton = page.getByRole('button', { name: 'Enter' }); - await expect(enterButton).toBeVisible(); - await enterButton.click(); + await signInToCurity(page); const nav = page.getByRole('navigation', { name: 'sidebar nav' }); await expect( diff --git a/src/devops-dashboard-backstage-app/playwright.config.ts b/src/devops-dashboard-backstage-app/playwright.config.ts index 967bf1b8..625ce451 100644 --- a/src/devops-dashboard-backstage-app/playwright.config.ts +++ b/src/devops-dashboard-backstage-app/playwright.config.ts @@ -33,7 +33,9 @@ export default defineConfig({ : [ { command: 'yarn start app', - url: 'http://localhost:3000', + // Reuses an already-running dev server (honor a port override + // in app-config.local.yaml by setting PLAYWRIGHT_URL). + url: process.env.PLAYWRIGHT_URL ?? 'http://localhost:3000', reuseExistingServer: true, timeout: 120_000, }, @@ -56,6 +58,8 @@ export default defineConfig({ baseURL: process.env.PLAYWRIGHT_URL ?? (process.env.CI ? 'http://localhost:7007' : 'http://localhost:3000'), + // The sign-in popup goes through the dev Curity server's self-signed TLS. + ignoreHTTPSErrors: true, screenshot: 'only-on-failure', trace: 'on-first-retry', }, diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts new file mode 100644 index 00000000..7376795e --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts @@ -0,0 +1,23 @@ +import { test, expect } from '@playwright/test'; +import { signInToCurity } from '../../../e2e-test-utils'; + +test('renders the DevOps Dashboard page with the Database Clients section', async ({ + page, +}) => { + await page.goto('/'); + await signInToCurity(page); + + const nav = page.getByRole('navigation', { name: 'sidebar nav' }); + const dashboardLink = nav.getByRole('link', { name: 'DevOps Dashboard' }); + await expect(dashboardLink).toBeVisible(); + await dashboardLink.click(); + + const defaultSectionUrl = /\/devops-dashboard\/db-clients$/; + await expect(page).toHaveURL(defaultSectionUrl); + await expect( + page.getByRole('tab', { name: 'Database Clients' }), + ).toBeVisible(); + await expect( + page.getByRole('heading', { name: 'Database Clients' }), + ).toBeVisible(); +}); From 1c2a64d7bef565ad056bb7f8961ac87613b4e6e7 Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 12 Aug 2026 19:07:08 +0200 Subject: [PATCH 3/6] IS-11630 Mock the Database Clients section with Backstage UI components. --- .../devops-dashboard/e2e-tests/plugin.test.ts | 5 +- .../DbClientsSection/DbClientsSection.tsx | 115 ++++++++++++++++-- .../devops-dashboard/src/i18n/index.ts | 29 +++++ .../src/utils/matchesIdOrName.ts | 11 ++ 4 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/i18n/index.ts create mode 100644 src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/utils/matchesIdOrName.ts diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts index 7376795e..00312b59 100644 --- a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/e2e-tests/plugin.test.ts @@ -18,6 +18,9 @@ test('renders the DevOps Dashboard page with the Database Clients section', asyn page.getByRole('tab', { name: 'Database Clients' }), ).toBeVisible(); await expect( - page.getByRole('heading', { name: 'Database Clients' }), + page.getByRole('searchbox', { name: 'Search database clients' }), + ).toBeVisible(); + await expect( + page.getByRole('rowheader', { name: 'spa-client' }), ).toBeVisible(); }); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx index 3382449c..3e807d36 100644 --- a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/components/DbClientsSection/DbClientsSection.tsx @@ -1,11 +1,108 @@ +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { + CellText, + ColumnConfig, + Flex, + SearchField, + Table, + Text, + useTable, +} from '@backstage/ui'; + +import { devopsDashboardTranslationRef } from '../../i18n'; +import { matchesIdOrName } from '../../utils/matchesIdOrName'; + /** - * Static placeholder for the Database Clients section. The real content - * (table, search, states) lands in the next step; this only proves the - * plugin structure: sidebar entry → page → tab → section. + * Static mock of the Database Clients section to agree on the shape before + * any data layer exists: the rows are hard-coded, and the search filters + * them client-side. The live version binds the same `Table`/`useTable` + * combination to real data. */ -export const DbClientsSection = () => ( -
-

Database Clients

-

Static section placeholder — content arrives in the next commit.

-
-); + +type PlaceholderClient = { + id: string; + name: string; + status: string; + capabilities: string; + created: string; +}; + +const PLACEHOLDER_CLIENTS: PlaceholderClient[] = [ + { + id: 'spa-client', + name: 'Single Page App', + status: 'Active', + capabilities: 'Code Flow', + created: '2026-05-14', + }, + { + id: 'mobile-app', + name: 'Mobile App', + status: 'Active', + capabilities: 'Code Flow, Refresh Tokens', + created: '2026-06-02', + }, + { + id: 'backend-service', + name: 'Backend Service', + status: 'Inactive', + capabilities: 'Client Credentials', + created: '2026-07-21', + }, +]; + +export const DbClientsSection = () => { + const { t } = useTranslationRef(devopsDashboardTranslationRef); + + const columns: ColumnConfig[] = [ + { + id: 'id', + label: t('dbClients.table.clientId'), + isRowHeader: true, + cell: client => , + }, + { + id: 'name', + label: t('dbClients.table.name'), + cell: client => , + }, + { + id: 'status', + label: t('dbClients.table.status'), + cell: client => , + }, + { + id: 'capabilities', + label: t('dbClients.table.capabilities'), + cell: client => , + }, + { + id: 'created', + label: t('dbClients.table.created'), + cell: client => , + }, + ]; + + const { tableProps, search } = useTable({ + mode: 'complete', + data: PLACEHOLDER_CLIENTS, + searchFn: matchesIdOrName, + }); + + return ( + + + {t('dbClients.description')} + + + + + + + ); +}; diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/i18n/index.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/i18n/index.ts new file mode 100644 index 00000000..30346312 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/i18n/index.ts @@ -0,0 +1,29 @@ +import { createTranslationRef } from '@backstage/frontend-plugin-api'; + +import { PLUGIN_ID } from '../plugin'; + +/** + * All user-facing texts of the plugin. Host apps can override single + * messages or add languages by registering a translation resource for + * this ref. + */ +export const devopsDashboardTranslationRef = createTranslationRef({ + id: PLUGIN_ID, + messages: { + dbClients: { + description: + 'OAuth clients that the Curity Identity Server stores in a database.', + search: { + label: 'Search database clients', + placeholder: 'Search by client ID or name', + }, + table: { + clientId: 'Client ID', + name: 'Name', + status: 'Status', + capabilities: 'Capabilities', + created: 'Created', + }, + }, + }, +}); diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/utils/matchesIdOrName.ts b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/utils/matchesIdOrName.ts new file mode 100644 index 00000000..bf1ab635 --- /dev/null +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/src/utils/matchesIdOrName.ts @@ -0,0 +1,11 @@ +/** + * Case-insensitive client-side search over items with an `id` and a `name`, + * as `useTable`'s `searchFn` expects. + */ +export const matchesIdOrName = ( + items: T[], + term: string, +): T[] => + items.filter(item => + `${item.id} ${item.name}`.toLowerCase().includes(term.toLowerCase()), + ); From 0434a612d1942f46d2189944504714c3840e05e4 Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Thu, 13 Aug 2026 12:55:33 +0200 Subject: [PATCH 4/6] IS-11630 Bump prettier to v3 and reformat, matching editor format-on-save. --- .../package.json | 2 +- .../packages/app/public/index.html | 2 +- .../app/src/modules/auth/curityAuthApi.ts | 12 ++++++-- .../src/modules/curityOidcAuthProvider.ts | 5 +++- .../plugins/devops-dashboard/README.md | 30 +++++++++---------- src/devops-dashboard-backstage-app/yarn.lock | 12 ++++---- 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/devops-dashboard-backstage-app/package.json b/src/devops-dashboard-backstage-app/package.json index 2147390c..3a5f4b67 100644 --- a/src/devops-dashboard-backstage-app/package.json +++ b/src/devops-dashboard-backstage-app/package.json @@ -45,7 +45,7 @@ "jest": "~30.2.0", "jsdom": "^27.1.0", "node-gyp": "^10.0.0", - "prettier": "^2.3.2", + "prettier": "^3.6.2", "typescript": "~5.8.0" }, "resolutions": { diff --git a/src/devops-dashboard-backstage-app/packages/app/public/index.html b/src/devops-dashboard-backstage-app/packages/app/public/index.html index 18da7c47..00859982 100644 --- a/src/devops-dashboard-backstage-app/packages/app/public/index.html +++ b/src/devops-dashboard-backstage-app/packages/app/public/index.html @@ -1,4 +1,4 @@ - + diff --git a/src/devops-dashboard-backstage-app/packages/app/src/modules/auth/curityAuthApi.ts b/src/devops-dashboard-backstage-app/packages/app/src/modules/auth/curityAuthApi.ts index ff5ee296..9e499e7a 100644 --- a/src/devops-dashboard-backstage-app/packages/app/src/modules/auth/curityAuthApi.ts +++ b/src/devops-dashboard-backstage-app/packages/app/src/modules/auth/curityAuthApi.ts @@ -22,7 +22,11 @@ import { CURITY_AUTH_PROVIDER_ID } from 'common'; * `curityAuthApiRef.getAccessToken(['urn:se:curity:scopes:admin:api'])` */ export const curityAuthApiRef = createApiRef< - OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi >({ id: 'auth.curity', }); @@ -55,7 +59,11 @@ export const curityAuthApi = ApiBlueprint.make({ icon: () => null, }, environment: configApi.getOptionalString('auth.environment'), - defaultScopes: ['openid', 'profile', 'urn:se:curity:scopes:admin:api'], + defaultScopes: [ + 'openid', + 'profile', + 'urn:se:curity:scopes:admin:api', + ], }), }), ), diff --git a/src/devops-dashboard-backstage-app/packages/backend/src/modules/curityOidcAuthProvider.ts b/src/devops-dashboard-backstage-app/packages/backend/src/modules/curityOidcAuthProvider.ts index 02d9b5f7..d74955bd 100644 --- a/src/devops-dashboard-backstage-app/packages/backend/src/modules/curityOidcAuthProvider.ts +++ b/src/devops-dashboard-backstage-app/packages/backend/src/modules/curityOidcAuthProvider.ts @@ -1,5 +1,8 @@ import { createBackendModule } from '@backstage/backend-plugin-api'; -import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { authProvidersExtensionPoint, createOAuthProviderFactory, diff --git a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md index a96e6197..b876a0c2 100644 --- a/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md +++ b/src/devops-dashboard-backstage-app/plugins/devops-dashboard/README.md @@ -36,28 +36,28 @@ Installation follows the standard Backstage steps: 1. Add the package to your app: - ```sh - yarn --cwd packages/app add @internal/backstage-plugin-devops-dashboard - ``` + ```sh + yarn --cwd packages/app add @internal/backstage-plugin-devops-dashboard + ``` 2. Let your app discover it. If your `app-config.yaml` enables package discovery, you are done: - ```yaml - app: - packages: all - ``` + ```yaml + app: + packages: all + ``` - Without discovery, register the plugin explicitly in your app's - `createApp` call: + Without discovery, register the plugin explicitly in your app's + `createApp` call: - ```tsx - import devopsDashboardPlugin from '@internal/backstage-plugin-devops-dashboard'; + ```tsx + import devopsDashboardPlugin from '@internal/backstage-plugin-devops-dashboard'; - const app = createApp({ - features: [devopsDashboardPlugin], - }); - ``` + const app = createApp({ + features: [devopsDashboardPlugin], + }); + ``` 3. Configure sign-in through Curity as described under [Requirements](#requirements). diff --git a/src/devops-dashboard-backstage-app/yarn.lock b/src/devops-dashboard-backstage-app/yarn.lock index dc04653c..a6ae2e79 100644 --- a/src/devops-dashboard-backstage-app/yarn.lock +++ b/src/devops-dashboard-backstage-app/yarn.lock @@ -25434,12 +25434,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.3.2": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" +"prettier@npm:^3.6.2": + version: 3.9.6 + resolution: "prettier@npm:3.9.6" bin: - prettier: bin-prettier.js - checksum: 10c0/463ea8f9a0946cd5b828d8cf27bd8b567345cf02f56562d5ecde198b91f47a76b7ac9eae0facd247ace70e927143af6135e8cf411986b8cb8478784a4d6d724a + prettier: bin/prettier.cjs + checksum: 10c0/9f7ddae234035868f3daab3dc34e6de7e54622185afb017378029f0c09a9c023248ccf6f34def0b17a0538e18c47aa1b3188bab72965d1bf735919baa0747e99 languageName: node linkType: hard @@ -27421,7 +27421,7 @@ __metadata: jest: "npm:~30.2.0" jsdom: "npm:^27.1.0" node-gyp: "npm:^10.0.0" - prettier: "npm:^2.3.2" + prettier: "npm:^3.6.2" typescript: "npm:~5.8.0" languageName: unknown linkType: soft From 0e2ff67142b123f0984662514ab4ca0660903dec Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 26 Aug 2026 11:52:35 +0200 Subject: [PATCH 5/6] IS-11630 Add a Backstage intro doc and point the READMEs to it. --- README.md | 1 + src/devops-dashboard-backstage-app/README.md | 71 ++++- .../docs/backstage-intro.md | 293 ++++++++++++++++++ 3 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 src/devops-dashboard-backstage-app/docs/backstage-intro.md diff --git a/README.md b/README.md index 5c936231..f484e26c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This monorepo contains: - CSS Library - UI Icons React Library - React Component Library +- DevOps Dashboard — a [Backstage](https://backstage.io) app and plugin (new to Backstage? start with the [intro](src/devops-dashboard-backstage-app/docs/backstage-intro.md)) ## Prerequisites - Node.js (version as specified in the `.nvmrc` file) diff --git a/src/devops-dashboard-backstage-app/README.md b/src/devops-dashboard-backstage-app/README.md index 041c4fbe..6771f6f8 100644 --- a/src/devops-dashboard-backstage-app/README.md +++ b/src/devops-dashboard-backstage-app/README.md @@ -1,10 +1,75 @@ -# [Backstage](https://backstage.io) +# DevOps Dashboard Backstage app -This is your newly scaffolded Backstage App, Good Luck! +The [Backstage](https://backstage.io) host app for the +[Curity DevOps Dashboard plugin](plugins/devops-dashboard/README.md). -To start the app, run: +The app serves two purposes: + +- The development harness for the plugin. +- The basis for the branded Backstage instance that Curity ships. + +On top of a standard Backstage app it adds sign-in through the Curity +Identity Server (`packages/app/src/modules/auth`, +`packages/backend/src/modules/curityOidcAuthProvider.ts`), so the plugin can +obtain access tokens for the Curity admin API. + +New to Backstage? Start with [Backstage in five minutes](docs/backstage-intro.md). + +## Run + +Prerequisites: a running Curity Identity Server with the +`devops_dashboard_backstage` OAuth client registered. Register it once with +the configuration shell (from the identity-server repo): + +```sh +dist/bin/idsh <<'EOF' +configure +set environments environment services zones default-zone allowed-origins-for-cors [ http://localhost:3000 http://localhost:3001 ] +set environments environment admin-service http restconf oauth oauth-profile oauth-dev +set environments environment admin-service http restconf oauth client [ devops_dashboard_backstage ] +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage capabilities code +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage secret Password1 +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage redirect-uris http://localhost:7007/api/auth/oidc/handler/frame +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope openid +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope profile +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope email +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage scope urn:se:curity:scopes:admin:api +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage audience [ devops_dashboard_backstage urn:se:curity:audiences:admin:api ] +set profiles profile oauth-dev oauth-service settings authorization-server client-store config-backed client devops_dashboard_backstage user-authentication allowed-authenticators testAuth-janedoe +commit comment "Register the Backstage dev client" +exit +exit +EOF +``` + +Notes on the values above: + +- The `allowed-origins-for-cors` zone setting lets the browser call the + database-clients GraphQL API from the Backstage origin. +- The audience must contain **both** the client id (OIDC sign-in validates + the ID token's `aud` against it) and the admin API audience (required for + the RESTCONF allow-listing). + +To show database clients, the profile also needs the feature enabled and the +GraphQL API authorized. Apply the two DevOps Dashboard enablement patches +(they configure the client data source, the GraphQL endpoint, the +authorization manager, and the groups claim the API requires): +`curity-web-ui/devops-dashboard/cypress/fixtures/enable-dashboard-patch-data.json` +and the `enable-database-clients` recipe in +`curity-web-ui/.claude/skills/enable-database-clients/`. ```sh yarn install +git apply dev-secrets.patch # creates the gitignored app-config.local.yaml yarn start ``` + +Frontend on `http://localhost:3000`, backend on `:7007`. Sign-in starts +automatically when the app opens. + +## Tests + +```sh +yarn test # unit tests +yarn test:e2e # Playwright, against the running app +``` diff --git a/src/devops-dashboard-backstage-app/docs/backstage-intro.md b/src/devops-dashboard-backstage-app/docs/backstage-intro.md new file mode 100644 index 00000000..11afa5c1 --- /dev/null +++ b/src/devops-dashboard-backstage-app/docs/backstage-intro.md @@ -0,0 +1,293 @@ +# Backstage in five minutes + +[Backstage](https://backstage.io) = framework for internal developer portals: one web app, all team tools, single UI. You write **plugins**; the framework composes them into the app. + +As a mental-model shortcut, each Backstage concept maps to an Angular one: + +| Angular | Backstage | In our code | +| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `main.ts` bootstrapping `AppModule` | App (root orchestrator) | `packages/app/src/App.tsx` | +| `NgModule` | Plugin | `createFrontendPlugin({ pluginId: 'devops-dashboard' })` | +| `NgModule` imports | `attachTo` (declared in the imported extension, not in the parent importer) | `attachTo:{ id: 'page:devops-dashboard', input: 'pages' }` // the child extension declares where it attaches (parent id + parent input) | +| Component | Extension | `devopsDashboardHomePage`, `dbClientsSubPage` | +| Service | Utility API extension | `configApi`, `graphQLApi` // injected with `useApi` util | +| Decorators (`@NgModule`, `@Component`...) | Utils: `createFrontendPlugin`, blueprints (`PageBlueprint`, `SubPageBlueprint`, `ApiBlueprint`) | `PageBlueprint.make({ ... })` | +| `Routes` array / `provideRouter` | Routes are registered on bootstrap by collecting every page extension's routes (`params.path`, `params.routeRef`, `params.loader`) | `homePage` `params.path: '/devops-dashboard'` + `rootRouteRef` (for linking) | + +## 1. The App — the composer + +The **App** = root orchestrator, like `main.ts` bootstrapping `AppModule`. No business logic: it reads config and composes everything into one **Extension Tree**. Ours lives in `packages/app`. + +## 2. Extensions — the building blocks + +An **extension** = one node in the Extension Tree: a page, a tab, an API client, a nav item. Think Angular components and services. Two flavors: + +- **Built-in extensions** — Backstage core: routing, error boundaries, theme registry. +- **Plugin extensions** — from plugins like ours. A plugin exposes everything _exclusively_ as extensions: pages, nav elements, APIs, features for other plugins. + +### Plugin — the extension container + +A **plugin** = the module/package with a feature's extensions — the unit you publish and version. It bundles **extensions** (the components and APIs it provides) and **routes** (the routeRefs other code links to by name). Ours: + +```ts +export const devopsDashboardPlugin = createFrontendPlugin({ + pluginId: 'devops-dashboard', + extensions: [homePage, dbClientsSubPage, configApi, graphQLApi], + routes: { root: rootRouteRef, dbClients: dbClientsRouteRef }, +}); +``` + +Rule of thumb: **you publish plugins, you wire extensions.** + +### Plugin installation + +Nobody imports our plugin into the app — it is **discovered**. Installing = adding a dependency; the chain runs through configuration: + +1. The consumer installs the package: `yarn add @internal/backstage-plugin-devops-dashboard` (run in `packages/app`; resolves as a workspace dependency in our monorepo). +2. [`packages/app/package.json`](../packages/app/package.json) now depends on the plugin: `"@internal/backstage-plugin-devops-dashboard": "workspace:^"`. +3. [`app-config.yaml`](../app-config.yaml) says `app: packages: all` — "scan my dependencies for Backstage packages". +4. The scanner detects the dependency plugins and wires them into the Extension Tree. + +## 3. The Extension Tree + +On bootstrap, the framework arranges every extension into a strict parent-child tree and instantiates it bottom-up: children before parents. Every extension has a single parent and a single communication — on bootstrap, with that parent only. Outputs carry structure, not live data. + +```mermaid +flowchart BT + subgraph our["Our plugin (devops-dashboard)"] + db["sub-page: db-clients
Database Clients tab"] + page["page:devops-dashboard
the dashboard page"] + api1["api: config"] + api2["api: graphql"] + end + subgraph core["Built-in extensions (Backstage core)"] + routes["routing"] + theme["themes"] + end + app["App (root)"] + + db -- "attachTo: pages" --> page + page --> app + api1 --> app + api2 --> app + routes --> app + theme --> app +``` + +Arrows = data flow: children hand outputs up to their parent. + +### Extension placement and communication + +**1. The parent declares its typed inputs** — named slots listing the exact data types a child must supply. The parent sees only the types it lists; extras are ignored: + +```ts +// From PageBlueprint's own source (the blueprint our page is built with): +// it declares a named input 'pages' — the slot where sub-pages may attach. +const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + inputs: { + pages: createExtensionInput([ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + coreExtensionData.title.optional(), + coreExtensionData.icon.optional(), + ]), + }, + // ...output, configSchema, factory +}); +``` + +**2. The child declares the parent's input it targets and provides the data** (`attachTo`) — its params become the output, emitted as typed extension data references (`coreExtensionData.routePath`, `coreExtensionData.reactElement`, …): + +```ts +export const dbClientsSubPage = SubPageBlueprint.make({ + attachTo: { id: 'page:devops-dashboard', input: 'pages' }, + name: 'db-clients', + params: { + path: 'db-clients', + title: 'Database Clients', + icon: , + routeRef: dbClientsRouteRef, + loader: () => + import('./components/DbClientsSection').then(m => ), + }, +}); +``` + +The child's output (`params`) IS the parent's input. + +**3. The address (`attachTo.id`) is a computed string**: `:[/]` — namespace defaults to the plugin ID; unnamed extensions drop `/`. Our page declares no `name` → it answers to `page:devops-dashboard`. The contract is verified at startup: mismatch → fail fast. + +## 4. Utils — the glue + +Utils = the framework's factory functions — Backstage's answer to Angular decorators. For example, `createFrontendPlugin` bundles extensions under one plugin ID (`@NgModule`), and Blueprints (`PageBlueprint`, `SubPageBlueprint`, `ApiBlueprint`, …) create common extension kinds without boilerplate. They carry the typing that handles the input/output setup. + +## 5. Core mechanics + +### Configuration (`configSchema` + app-config) + +Each extension declares its accepted config via a schema (Zod), defaults included. Integrators tune it in `app-config.yaml`. Provided config replaces, never merges. + +The contract — the plugin declares its config shape in [`config.d.ts`](../plugins/devops-dashboard/config.d.ts) (`@visibility frontend` makes a key readable in the browser): + +```ts +export interface Config { + devopsDashboard?: { + /** @visibility frontend */ + profiles?: Array<{ + /** @visibility frontend */ + id: string; + /** @visibility frontend */ + dbClientsGraphqlUrl?: string; + }>; + }; +} +``` + +The provision — the integrator fills the contract in [`app-config.yaml`](../app-config.yaml): + +```yaml +devopsDashboard: + profiles: + - id: oauth-dev + dbClientsGraphqlUrl: https://localhost:9443/oauth-dev/clients-graphql-api +``` + +### Routing — `path`, `routeRef`, and the plugin's router API + +Pages don't register routes — they _output_ a path, a route ref, and a React element. The app's route registry assembles the router config from them on bootstrap. Our plugin shows the whole mechanism in three declarations. + +**1. Declare the names** ([`routes.ts`](../plugins/devops-dashboard/src/routes.ts)). A route ref = an empty token: pure identity, no URL. + +```ts +export const rootRouteRef = createRouteRef(); +export const dbClientsRouteRef = createRouteRef(); +``` + +**2. Bind each name to a destination** (the page extensions). Each page declares its URL and hands its ref to it. + +```ts +export const homePage = PageBlueprint.make({ + // An absolute path declares a top-level Routes entry + params: { path: '/devops-dashboard', routeRef: rootRouteRef /* ... */ }, +}); + +export const dbClientsSubPage = SubPageBlueprint.make({ + // A relative path declares a children route Routes entry + // The router composes /devops-dashboard/db-clients and binds each ref + // to its final URL. + attachTo: { id: 'page:devops-dashboard', input: 'pages' }, + params: { path: 'db-clients', routeRef: dbClientsRouteRef /* ... */ }, +}); +``` + +`path` lives in the config schema → an integrator can remap any URL in `app-config.yaml`, and no link breaks: code navigates refs, never URL strings. + +**3. Export the router API** (the plugin). The `routes` map publishes the refs under stable names. The keys become the public route names (`devops-dashboard.root`, `devops-dashboard.dbClients`) other parts of the app can link to. + +```ts +export const devopsDashboardPlugin = createFrontendPlugin({ + pluginId: 'devops-dashboard', + extensions: [homePage, dbClientsSubPage, configApi, graphQLApi], + routes: { root: rootRouteRef, dbClients: dbClientsRouteRef }, +}); +``` + +**Third-party consumption.** A consumer never imports our package. It declares an **external route ref** — a link placeholder — with our public route name as its default target: + +```ts +// a hypothetical "reports" plugin +export const dbClientsExternalRef = createExternalRouteRef({ + defaultTarget: 'devops-dashboard.dbClients', +}); + +export const reportsPlugin = createFrontendPlugin({ + pluginId: 'reports', + extensions: [reportsPage], + externalRoutes: { dbClients: dbClientsExternalRef }, +}); +``` + +```tsx +const dbClientsLink = useRouteRef(dbClientsExternalRef); +if (!dbClientsLink) return null; // dashboard not installed — hide the link +return Inspect database clients; +``` + +Resolution order: explicit binding in `app-config.yaml` (`app.routes.bindings`) → `defaultTarget` → `undefined`. Never a startup error. Producer exports `routes`, consumer exports `externalRoutes`, the app connects placeholder to destination by name. + +## The role of React + +Backstage is a React app: the framework decides _what exists and where_ (the Extension Tree, frozen on bootstrap); React renders it. The deviations from a standard React app concentrate in state management. + +- **App level — owned by the app.** A plugin cannot wrap the whole app in a context provider (`AppRootElementBlueprint` mounts global _elements_ — overlays, alerts — not wrappers). App-wide context providers belong to `packages/app`, typically as a frontend module registered in `createApp({ features: [...] })` — like our `authModule` (Curity sign-in) in [`App.tsx`](../packages/app/src/App.tsx). + - **App-level state — utility APIs.** The exception to the context restriction: a plugin ships an API extension, and `useApi` hands any component the same shared instance regardless of tree position — app-level sharing (state and functionality) without context providers, like Angular's `providedIn: 'root'`: + + ```tsx + // any component, any extension — same instance everywhere + const configApi = useApi(devopsDashboardConfigApiRef); + const profiles = await configApi.getProfiles(); + ``` + +- **Plugin level — `PluginWrapperBlueprint`.** A regular React context provider around all the plugin's extensions; its `useWrapperValue` hook runs in a single place, so the value is genuinely shared: + +```tsx +export const devopsDashboardWrapper = PluginWrapperBlueprint.make({ + params: define => + define({ + loader: async () => ({ + useWrapperValue: () => useApi(devopsDashboardGraphQLApiRef).getClient(), + component: ({ children, value }) => ( + {children} + ), + }), + }), +}); +``` + +- **Page level — not authorable by default.** Sub-pages render inside the page's element, so they receive whatever context exists above them: app, plugin-wrapper, and the framework's own page-level context (e.g. breadcrumbs). What you cannot do is add _your own_ context provider at the page level: a page's component loader is mutually exclusive with sub-page default rendering (true page-level wrapping would require forking `PageBlueprint` with `makeWithOverrides`). +- **Extension level and below — default React.** Any extension can wrap its own element with a context provider, and inside components everything is plain React: state, hooks, effects, context, `useApi`. + +## Rendering notes + +- **Everything mounts lazily, semi-isolated.** Each extension's component loads via `React.lazy()` on first visit and renders inside its own `ExtensionBoundary` (error boundary + suspense): one broken extension doesn't crash the app, and module-level code runs on first render, not at app start. +- **Sub-page tabs are routes, not stateful tabs.** Switching tabs is a navigation: React Router unmounts the previous sub-page and its local state — standard Router behavior; the deviation is that tabs _are_ routes. State that must survive a tab switch belongs in a utility API. + +Official guide: [Building frontend plugins](https://backstage.io/docs/frontend-system/building-plugins/index) — pages, `React.lazy()` loaders, and components. + +## 6. From code to running app + +```mermaid +flowchart LR + a["1. Tree construction
read every attachTo"] --> b["2. Validation
children's outputs satisfy parents' inputs?"] + b --> c["3. Bottom-up instantiation
leaves first, root last"] + c --> d["4. Rendering
root assembles the UI"] +``` + +1. **Tree construction** — read all extensions + their `attachTo`. Integrators can override `attachTo` in `app-config.yaml`: move extensions without touching code. +2. **Validation** — every child output must satisfy its parent input. Mismatch → fail fast at startup. +3. **Bottom-up instantiation** — the step to remember. Child factory runs first: gets its config + its own children's inputs, yields outputs. Outputs flow up through the attachment point → become an input entry on the parent → parent factory runs → passes its output further up. +4. **Rendering** — root extensions (app core, routes, page layouts) assemble the collected React elements and route refs into the UI. + +## Cheat sheet + +| Backstage term | Meaning | Angular reflex | +| ---------------------- | ------------------------------------------------------------------------------------------------ | --------------------------- | +| `createFrontendPlugin` | Bundles a plugin's extensions under one `pluginId` | `@NgModule` | +| Blueprints | Helpers that create common extension kinds without boilerplate | `@Component`, `@Injectable` | +| Extension ID | `:[/]`, e.g. `page:devops-dashboard` | selector, but computed | +| `attachTo` | `{ id, input }` — where this extension plugs in, declared by the child | `imports` array, inverted | +| Output | Typed data a child yields to its parent (element, route ref, API factory) | | +| Input | A named slot a parent exposes; it only sees the data types it declared | | +| `routeRef` | A stable, linkable name for a destination — code navigates through it, never through URL strings | route constant | +| `externalRoutes` | A `routeRef` placeholder to another plugin's destination; the app binds it by name | | +| Utility API extension | A typed client other extensions consume | service / provider | +| `app-config.yaml` | Where integrators configure and re-arrange extensions | `environment.ts`, DI config | + +## Where to go deeper + +- [Backstage frontend system: architecture](https://backstage.io/docs/frontend-system/architecture/index) +- [Extensions](https://backstage.io/docs/frontend-system/architecture/extensions) +- [Extension blueprints](https://backstage.io/docs/frontend-system/architecture/extension-blueprints) +- Our plugin's entry point: [`plugins/devops-dashboard/src/plugin.tsx`](../plugins/devops-dashboard/src/plugin.tsx) From 930ca158ca1dafc30b99ab2bc5ccefed23db4718 Mon Sep 17 00:00:00 2001 From: Aleix Suau Date: Wed, 26 Aug 2026 12:39:17 +0200 Subject: [PATCH 6/6] IS-11630 Keep the Backstage intro pointer inside the app folder. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f484e26c..5c936231 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ This monorepo contains: - CSS Library - UI Icons React Library - React Component Library -- DevOps Dashboard — a [Backstage](https://backstage.io) app and plugin (new to Backstage? start with the [intro](src/devops-dashboard-backstage-app/docs/backstage-intro.md)) ## Prerequisites - Node.js (version as specified in the `.nvmrc` file)