diff --git a/.changeset/angry-ducks-shake.md b/.changeset/angry-ducks-shake.md
new file mode 100644
index 0000000000..58975123b4
--- /dev/null
+++ b/.changeset/angry-ducks-shake.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-test-utils': patch
+---
+
+Allow specifying custom Docker registry for database tests
diff --git a/.changeset/big-days-press.md b/.changeset/big-days-press.md
new file mode 100644
index 0000000000..7dec7d6288
--- /dev/null
+++ b/.changeset/big-days-press.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-scaffolder-backend': minor
+---
+
+Expose both types of scaffolder permissions and rules through the metadata endpoint.
+
+The metadata endpoint now correctly exposes both types of scaffolder permissions and rules (for both the template and action resource types) through the metadata endpoint.
diff --git a/.changeset/blue-eyes-run.md b/.changeset/blue-eyes-run.md
new file mode 100644
index 0000000000..1b96e01f1c
--- /dev/null
+++ b/.changeset/blue-eyes-run.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-devtools': minor
+'@backstage/plugin-devtools-backend': minor
+'@backstage/plugin-devtools-common': minor
+---
+
+Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
diff --git a/.changeset/brown-keys-raise.md b/.changeset/brown-keys-raise.md
new file mode 100644
index 0000000000..1d288d9f57
--- /dev/null
+++ b/.changeset/brown-keys-raise.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-kubernetes': patch
+---
+
+The Kubernetes plugin now requests AKS access tokens from Azure when retrieving
+objects from clusters configured with `authProvider: aks` and sets `auth.aks` in
+its request bodies appropriately.
diff --git a/.changeset/brown-meals-trade.md b/.changeset/brown-meals-trade.md
new file mode 100644
index 0000000000..f6849fa19b
--- /dev/null
+++ b/.changeset/brown-meals-trade.md
@@ -0,0 +1,5 @@
+---
+'@backstage/repo-tools': minor
+---
+
+**BREAKING**: The OpenAPI commands are now found within the `schema openapi` sub-command. For example `yarn backstage-repo-tools schema:openapi:verify` is now `yarn backstage-repo-tools schema openapi verify`.
diff --git a/.changeset/calm-buttons-type.md b/.changeset/calm-buttons-type.md
new file mode 100644
index 0000000000..11b300a684
--- /dev/null
+++ b/.changeset/calm-buttons-type.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend': patch
+---
+
+Bump minimum required version of `vm2` to be 3.9.18
diff --git a/.changeset/calm-snails-trade.md b/.changeset/calm-snails-trade.md
new file mode 100644
index 0000000000..0738b5aa37
--- /dev/null
+++ b/.changeset/calm-snails-trade.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-bazaar': patch
+---
+
+Fixed `validateDOMNesting` warnings
diff --git a/.changeset/chilled-goats-jump.md b/.changeset/chilled-goats-jump.md
new file mode 100644
index 0000000000..3dd152712a
--- /dev/null
+++ b/.changeset/chilled-goats-jump.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': patch
+---
+
+Correct command to create new plugins
diff --git a/.changeset/chilled-pants-wonder.md b/.changeset/chilled-pants-wonder.md
new file mode 100644
index 0000000000..501ced7cba
--- /dev/null
+++ b/.changeset/chilled-pants-wonder.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-openapi-utils': patch
+---
+
+Use permalinks for links including a line number reference
diff --git a/.changeset/chilled-queens-wonder.md b/.changeset/chilled-queens-wonder.md
new file mode 100644
index 0000000000..a10212792d
--- /dev/null
+++ b/.changeset/chilled-queens-wonder.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Ensure that entity cache state is only written to the database when actually changed
diff --git a/.changeset/chilly-taxis-shop.md b/.changeset/chilly-taxis-shop.md
new file mode 100644
index 0000000000..9e2e73a5e3
--- /dev/null
+++ b/.changeset/chilly-taxis-shop.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-home': patch
+---
+
+Remove object-hash dependency
diff --git a/.changeset/clever-coins-deny.md b/.changeset/clever-coins-deny.md
new file mode 100644
index 0000000000..0c1e2ade26
--- /dev/null
+++ b/.changeset/clever-coins-deny.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-home': patch
+---
+
+Use the semantic time tag for rendering world clocks on homepage headers.
diff --git a/.changeset/cold-laws-turn.md b/.changeset/cold-laws-turn.md
new file mode 100644
index 0000000000..6220f088db
--- /dev/null
+++ b/.changeset/cold-laws-turn.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-events-backend-module-aws-sqs': minor
+---
+
+Allow endpoint configuration for sqs, enabling use of localstack for testing.
diff --git a/.changeset/cool-dingos-repair.md b/.changeset/cool-dingos-repair.md
new file mode 100644
index 0000000000..fe8377f5f1
--- /dev/null
+++ b/.changeset/cool-dingos-repair.md
@@ -0,0 +1,5 @@
+---
+'@backstage/theme': minor
+---
+
+Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
diff --git a/.changeset/curly-rats-fold.md b/.changeset/curly-rats-fold.md
new file mode 100644
index 0000000000..c4cd8d514b
--- /dev/null
+++ b/.changeset/curly-rats-fold.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Internal refactoring for performance in the service handlers
diff --git a/.changeset/eleven-taxis-notice.md b/.changeset/eleven-taxis-notice.md
new file mode 100644
index 0000000000..88a25991fa
--- /dev/null
+++ b/.changeset/eleven-taxis-notice.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch
+'@backstage/config-loader': patch
+'@backstage/plugin-auth-backend': patch
+---
+
+Fixed the way that some request errors are thrown
diff --git a/.changeset/extragavent-fast-fly.md b/.changeset/extragavent-fast-fly.md
new file mode 100644
index 0000000000..697b5163ec
--- /dev/null
+++ b/.changeset/extragavent-fast-fly.md
@@ -0,0 +1,10 @@
+---
+'@backstage/plugin-badges': patch
+'@backstage/plugin-badges-backend': minor
+---
+
+Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+**BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
diff --git a/.changeset/fair-suits-warn.md b/.changeset/fair-suits-warn.md
new file mode 100644
index 0000000000..3edc2aa1ca
--- /dev/null
+++ b/.changeset/fair-suits-warn.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-tech-radar': patch
+---
+
+Fix description links when clicking entry in radar.
diff --git a/.changeset/fast-wasps-peel.md b/.changeset/fast-wasps-peel.md
new file mode 100644
index 0000000000..075336a506
--- /dev/null
+++ b/.changeset/fast-wasps-peel.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Enable strict config checking during `backstage-cli config:check` with the new `--strict` option which will surface schema errors.
diff --git a/.changeset/fifty-grapes-explode.md b/.changeset/fifty-grapes-explode.md
new file mode 100644
index 0000000000..999a105c4f
--- /dev/null
+++ b/.changeset/fifty-grapes-explode.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests.
diff --git a/.changeset/five-lies-confess.md b/.changeset/five-lies-confess.md
new file mode 100644
index 0000000000..eefd2bd6fa
--- /dev/null
+++ b/.changeset/five-lies-confess.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Improve the query for orphan pruning
diff --git a/.changeset/fluffy-crabs-reply.md b/.changeset/fluffy-crabs-reply.md
new file mode 100644
index 0000000000..0665dd864d
--- /dev/null
+++ b/.changeset/fluffy-crabs-reply.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-backend-module-elasticsearch': minor
+---
+
+Upgrade to aws-sdk v3 and include OpenSearch Serverless support
diff --git a/.changeset/fluffy-mirrors-happen.md b/.changeset/fluffy-mirrors-happen.md
new file mode 100644
index 0000000000..6dce398217
--- /dev/null
+++ b/.changeset/fluffy-mirrors-happen.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-kubernetes-backend': patch
+---
+
+Kubernetes clusters now support `authProvider: aks`. When configured this way,
+the `retrieveObjectsByServiceId` action will use the `auth.aks` value in the
+request body as a bearer token to authenticate with Kubernetes.
diff --git a/.changeset/friendly-frogs-drop.md b/.changeset/friendly-frogs-drop.md
new file mode 100644
index 0000000000..682f1dbabb
--- /dev/null
+++ b/.changeset/friendly-frogs-drop.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Change orphan cleanup task to only log a message if it deleted entities.
diff --git a/.changeset/funny-cups-count.md b/.changeset/funny-cups-count.md
new file mode 100644
index 0000000000..19dbc3b8bf
--- /dev/null
+++ b/.changeset/funny-cups-count.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-gcalendar': patch
+---
+
+Pass user info email scope on auth refresh to resolve invalid credentials error
diff --git a/.changeset/giant-students-lie.md b/.changeset/giant-students-lie.md
new file mode 100644
index 0000000000..fdcb406296
--- /dev/null
+++ b/.changeset/giant-students-lie.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend': minor
+---
+
+Add ability to use `defaultNamespace` and `defaultKind` for scaffolder action `catalog:fetch`
diff --git a/.changeset/gold-vans-sin.md b/.changeset/gold-vans-sin.md
new file mode 100644
index 0000000000..d99cb9834d
--- /dev/null
+++ b/.changeset/gold-vans-sin.md
@@ -0,0 +1,10 @@
+---
+'@backstage/plugin-scaffolder-backend-module-gitlab': patch
+---
+
+Fix input schema validation issue for gitlab actions:
+
+- gitlab:group:ensureExists
+- gitlab:projectAccessToken:create
+- gitlab:projectDeployToken:create
+- gitlab:projectVariable:create
diff --git a/.changeset/heavy-penguins-burn.md b/.changeset/heavy-penguins-burn.md
new file mode 100644
index 0000000000..b8c243a99b
--- /dev/null
+++ b/.changeset/heavy-penguins-burn.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-scaffolder-common': minor
+'@backstage/plugin-scaffolder-react': minor
+---
+
+Add support for Markdown text blob outputs from templates
diff --git a/.changeset/honest-comics-ring.md b/.changeset/honest-comics-ring.md
new file mode 100644
index 0000000000..38a962c5d3
--- /dev/null
+++ b/.changeset/honest-comics-ring.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-todo-backend': patch
+---
+
+Added OpenAPI schema
diff --git a/.changeset/honest-turkeys-learn.md b/.changeset/honest-turkeys-learn.md
new file mode 100644
index 0000000000..e4e6aae5fc
--- /dev/null
+++ b/.changeset/honest-turkeys-learn.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Remove unnecessary join in the entity facets endpoint, exclude nulls
diff --git a/.changeset/hungry-countries-enjoy.md b/.changeset/hungry-countries-enjoy.md
new file mode 100644
index 0000000000..c14ec702b9
--- /dev/null
+++ b/.changeset/hungry-countries-enjoy.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+Fixed a bug in the Azure auth provider which prevented getting access tokens with multiple scopes for one resource
diff --git a/.changeset/hungry-peas-sing.md b/.changeset/hungry-peas-sing.md
new file mode 100644
index 0000000000..8c39d33ddd
--- /dev/null
+++ b/.changeset/hungry-peas-sing.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend-module-openapi': patch
+---
+
+Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file
diff --git a/.changeset/itchy-impalas-fold.md b/.changeset/itchy-impalas-fold.md
new file mode 100644
index 0000000000..31840124ad
--- /dev/null
+++ b/.changeset/itchy-impalas-fold.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-events-backend-module-aws-sqs': patch
+'@backstage/integration-aws-node': patch
+'@backstage/plugin-kubernetes-backend': patch
+'@backstage/backend-common': patch
+'@backstage/plugin-techdocs-node': patch
+---
+
+Standardize `@aws-sdk` v3 versions
diff --git a/.changeset/khaki-brooms-kiss.md b/.changeset/khaki-brooms-kiss.md
new file mode 100644
index 0000000000..76d4ff2cd2
--- /dev/null
+++ b/.changeset/khaki-brooms-kiss.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-explore': patch
+---
+
+Display the title of the entity on the explore card if present, otherwise stick to the name
diff --git a/.changeset/late-lizards-unite.md b/.changeset/late-lizards-unite.md
new file mode 100644
index 0000000000..bf0a03de12
--- /dev/null
+++ b/.changeset/late-lizards-unite.md
@@ -0,0 +1,5 @@
+---
+'@backstage/config-loader': patch
+---
+
+Added a new `noUndeclaredProperties` option to `SchemaLoader` to support enforcing that there are no extra keys when verifying config.
diff --git a/.changeset/moody-shrimps-train.md b/.changeset/moody-shrimps-train.md
new file mode 100644
index 0000000000..397500e2cb
--- /dev/null
+++ b/.changeset/moody-shrimps-train.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend-module-gitlab': minor
+---
+
+Add a new scaffolder action for gitlab to ensure a group exists
diff --git a/.changeset/new-roses-fail.md b/.changeset/new-roses-fail.md
new file mode 100644
index 0000000000..77495c5f54
--- /dev/null
+++ b/.changeset/new-roses-fail.md
@@ -0,0 +1,16 @@
+---
+'@backstage/plugin-org': patch
+---
+
+Changed the MembersListCard component to allow displaying aggregated members when viewing a group. Now, a toggle switch can be displayed that lets you switch between showing direct members and aggregated members.
+
+To enable this new feature, set the showAggregateMembersToggle prop on EntityMembersListCard:
+
+```jsx
+// In packages/app/src/components/catalog/EntityPage.tsx
+const groupPage = (
+ // ...
+
+ // ...
+);
+```
diff --git a/.changeset/nice-ants-type.md b/.changeset/nice-ants-type.md
new file mode 100644
index 0000000000..632d323213
--- /dev/null
+++ b/.changeset/nice-ants-type.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend-module-aws': minor
+---
+
+**BREAKING**: AwsOrganizationCloudAccountProcessor.fromConfig now returns a promise instead of the instance directly.
diff --git a/.changeset/nine-planes-carry.md b/.changeset/nine-planes-carry.md
new file mode 100644
index 0000000000..414cc25680
--- /dev/null
+++ b/.changeset/nine-planes-carry.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-azure-sites-backend': patch
+---
+
+Updated URL to `/health` and corrected typos in the `README.md`
diff --git a/.changeset/ninety-eggs-lie.md b/.changeset/ninety-eggs-lie.md
new file mode 100644
index 0000000000..83b04b82af
--- /dev/null
+++ b/.changeset/ninety-eggs-lie.md
@@ -0,0 +1,6 @@
+---
+'@backstage/core-components': patch
+'@backstage/theme': patch
+---
+
+Fix accessibility issue with Backstage Table's header style
diff --git a/.changeset/ninety-mugs-sin.md b/.changeset/ninety-mugs-sin.md
new file mode 100644
index 0000000000..6b71a00667
--- /dev/null
+++ b/.changeset/ninety-mugs-sin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-react': minor
+---
+
+ accepts InputProp property that can override keys from default
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 759b1b40e2..bec2a1e2df 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -1,5 +1,5 @@
{
- "mode": "pre",
+ "mode": "exit",
"tag": "next",
"initialVersions": {
"example-app": "0.2.82",
@@ -220,52 +220,89 @@
"@backstage/plugin-vault": "0.1.11",
"@backstage/plugin-vault-backend": "0.3.0",
"@backstage/plugin-xcmetrics": "0.2.37",
- "@backstage/plugin-analytics-module-ga4": "0.0.0"
+ "@backstage/plugin-analytics-module-ga4": "0.0.0",
+ "@backstage/plugin-devtools": "0.0.0",
+ "@backstage/plugin-devtools-backend": "0.0.0",
+ "@backstage/plugin-devtools-common": "0.0.0"
},
"changesets": [
"afraid-shirts-train",
"backend-like-a-tattoo",
"beige-tigers-matter",
+ "blue-eyes-run",
"brown-eyes-yell",
+ "brown-keys-raise",
+ "calm-snails-trade",
"calm-wolves-taste",
+ "chilled-goats-jump",
+ "chilled-pants-wonder",
+ "chilled-queens-wonder",
"chilly-fireants-type",
+ "chilly-taxis-shop",
"clean-lobsters-brake",
"cold-drinks-exercise",
+ "cold-laws-turn",
+ "cool-dingos-repair",
"create-app-1682437922",
+ "curly-rats-fold",
"curvy-months-appear",
"dry-boats-sniff",
"dull-wombats-stare",
"eighty-olives-live",
+ "extragavent-fast-fly",
+ "fair-suits-warn",
+ "fifty-grapes-explode",
"fifty-laws-count",
"five-tigers-whisper",
+ "fluffy-crabs-reply",
+ "fluffy-mirrors-happen",
"fresh-moons-unite",
"funny-trains-sniff",
"good-lions-approve",
"green-cheetahs-cover",
"healthy-ladybugs-chew",
+ "heavy-penguins-burn",
"honest-countries-deny",
+ "honest-turkeys-learn",
"hungry-foxes-divide",
+ "hungry-peas-sing",
+ "khaki-brooms-kiss",
"lazy-keys-work",
"mighty-bears-glow",
"mighty-cows-greet",
"modern-pumpkins-join",
"moody-lizards-beam",
+ "moody-shrimps-train",
+ "new-roses-fail",
"nine-ties-smoke",
+ "ninety-eggs-lie",
+ "ninety-mugs-sin",
"olive-turkeys-switch",
"polite-dingos-train",
"pretty-gifts-greet",
"quick-flies-guess",
+ "quiet-bikes-smash",
+ "quiet-stingrays-fly",
+ "rare-elephants-arrive",
"red-cheetahs-invite",
"rotten-chefs-jog",
"serious-hornets-remember",
"six-mails-shout",
"slimy-turkeys-return",
"soft-readers-exist",
+ "stupid-seas-stare",
"swift-fishes-run",
"tame-games-rule",
"thick-parents-pump",
+ "thick-pugs-rush",
+ "thin-ways-exist",
+ "thirty-jobs-sort",
"tough-moles-whisper",
+ "twelve-zebras-repair",
+ "two-gorillas-drop",
+ "unlucky-deers-teach",
"unlucky-plants-give",
- "warm-lamps-tease"
+ "warm-lamps-tease",
+ "wet-dolphins-love"
]
}
diff --git a/.changeset/proud-chairs-sleep.md b/.changeset/proud-chairs-sleep.md
new file mode 100644
index 0000000000..6df02d052d
--- /dev/null
+++ b/.changeset/proud-chairs-sleep.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-react': patch
+---
+
+`SearchPagination` now automatically resets the page cursor when the page limit is changed
diff --git a/.changeset/quiet-bikes-smash.md b/.changeset/quiet-bikes-smash.md
new file mode 100644
index 0000000000..ca751adaf7
--- /dev/null
+++ b/.changeset/quiet-bikes-smash.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-openapi-utils': patch
+---
+
+Corrected resolution of parameter nested schema to use central schemas.
diff --git a/.changeset/quiet-stingrays-fly.md b/.changeset/quiet-stingrays-fly.md
new file mode 100644
index 0000000000..536ee18a6f
--- /dev/null
+++ b/.changeset/quiet-stingrays-fly.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-azure-devops': minor
+---
+
+The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
diff --git a/.changeset/rare-elephants-arrive.md b/.changeset/rare-elephants-arrive.md
new file mode 100644
index 0000000000..d27f763d1f
--- /dev/null
+++ b/.changeset/rare-elephants-arrive.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend-module-puppetdb': patch
+---
+
+Fixes import paths and updates documentation
diff --git a/.changeset/real-baboons-vanish.md b/.changeset/real-baboons-vanish.md
new file mode 100644
index 0000000000..ca09cc9bf3
--- /dev/null
+++ b/.changeset/real-baboons-vanish.md
@@ -0,0 +1,8 @@
+---
+'@backstage/plugin-jenkins-backend': minor
+'@backstage/plugin-jenkins': minor
+---
+
+Updated rebuild to use Jenkins API replay build, which works for Jenkins jobs that have required parameters. Jenkins SDK could not be used for this request because it does not have support for replay.
+
+Added link to view build in Jenkins CI/CD table action column.
diff --git a/.changeset/red-parrots-attack.md b/.changeset/red-parrots-attack.md
new file mode 100644
index 0000000000..1530c17c8b
--- /dev/null
+++ b/.changeset/red-parrots-attack.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend-module-gitlab': patch
+---
+
+Fix a corner case where returned users are null for bots
diff --git a/.changeset/serious-phones-flash.md b/.changeset/serious-phones-flash.md
new file mode 100644
index 0000000000..4a4afe5d4e
--- /dev/null
+++ b/.changeset/serious-phones-flash.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs-module-addons-contrib': patch
+---
+
+Show cursor pointer when hovering on lightbox
diff --git a/.changeset/shy-worms-enjoy.md b/.changeset/shy-worms-enjoy.md
new file mode 100644
index 0000000000..cbfb2b5f7b
--- /dev/null
+++ b/.changeset/shy-worms-enjoy.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-user-settings': patch
+'@backstage/plugin-auth-backend': patch
+---
+
+Fix config schema definition.
diff --git a/.changeset/sixty-falcons-protect.md b/.changeset/sixty-falcons-protect.md
new file mode 100644
index 0000000000..0bc77bfbd9
--- /dev/null
+++ b/.changeset/sixty-falcons-protect.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-badges-backend': patch
+---
+
+Removed some unused dependencies
diff --git a/.changeset/spicy-spies-warn.md b/.changeset/spicy-spies-warn.md
new file mode 100644
index 0000000000..3ed26a2cc3
--- /dev/null
+++ b/.changeset/spicy-spies-warn.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-scaffolder-common': minor
+---
+
+Expose scaffolder permissions in new sub-aggregations.
+
+In addition to exporting a list of all scaffolder permissions in `scaffolderPermissions`, scaffolder-common now exports `scaffolderTemplatePermissions` and `scaffolderActionPermissions`, which contain subsets of the scaffolder permissions separated by resource type.
diff --git a/.changeset/spotty-walls-serve.md b/.changeset/spotty-walls-serve.md
new file mode 100644
index 0000000000..9b0da31a5f
--- /dev/null
+++ b/.changeset/spotty-walls-serve.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-kubernetes-backend': patch
+---
+
+Fixed a bug in the Kubernetes proxy endpoint where requests to clusters configured with client-side auth providers would always fail with a 500 status.
diff --git a/.changeset/stupid-seas-stare.md b/.changeset/stupid-seas-stare.md
new file mode 100644
index 0000000000..f3d4f5e1f7
--- /dev/null
+++ b/.changeset/stupid-seas-stare.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action.
diff --git a/.changeset/swift-students-type.md b/.changeset/swift-students-type.md
new file mode 100644
index 0000000000..577fbeb8b6
--- /dev/null
+++ b/.changeset/swift-students-type.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-openapi-utils': patch
+---
+
+Updated README to reflect changes in `@backstage/repo-tools`.
diff --git a/.changeset/tame-games-rule.md b/.changeset/tame-games-rule.md
deleted file mode 100644
index 408c8ca14b..0000000000
--- a/.changeset/tame-games-rule.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Pin some octokit dependencies to avoid failing typescript build under `tsc:full`
diff --git a/.changeset/thick-pugs-rush.md b/.changeset/thick-pugs-rush.md
new file mode 100644
index 0000000000..7f0171eb83
--- /dev/null
+++ b/.changeset/thick-pugs-rush.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Added a persistent session store through the database
diff --git a/.changeset/thin-ways-exist.md b/.changeset/thin-ways-exist.md
new file mode 100644
index 0000000000..fd1ecb62a4
--- /dev/null
+++ b/.changeset/thin-ways-exist.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-react': patch
+---
+
+Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels.
diff --git a/.changeset/thirty-jobs-sort.md b/.changeset/thirty-jobs-sort.md
new file mode 100644
index 0000000000..8a40302502
--- /dev/null
+++ b/.changeset/thirty-jobs-sort.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-kubernetes-common': patch
+---
+
+AKS access tokens can now be sent over the wire to the Kubernetes backend.
diff --git a/.changeset/three-tools-pump.md b/.changeset/three-tools-pump.md
new file mode 100644
index 0000000000..ac9d3c9909
--- /dev/null
+++ b/.changeset/three-tools-pump.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+Fix accessibility issue with Edit Metadata Link on screen readers missing notice about opening in a new tab.
diff --git a/.changeset/tiny-crews-pull.md b/.changeset/tiny-crews-pull.md
new file mode 100644
index 0000000000..8d499ac5e6
--- /dev/null
+++ b/.changeset/tiny-crews-pull.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Deprecated the use of React 16
diff --git a/.changeset/twelve-birds-boil.md b/.changeset/twelve-birds-boil.md
new file mode 100644
index 0000000000..04317c26cd
--- /dev/null
+++ b/.changeset/twelve-birds-boil.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-adr': minor
+---
+
+render SupportButton only if config is set
diff --git a/.changeset/twelve-zebras-repair.md b/.changeset/twelve-zebras-repair.md
new file mode 100644
index 0000000000..8d74080c38
--- /dev/null
+++ b/.changeset/twelve-zebras-repair.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search-backend': patch
+---
+
+Added an OpenAPI 3.0 spec and enforced schema-first model on the router.
diff --git a/.changeset/two-gorillas-drop.md b/.changeset/two-gorillas-drop.md
new file mode 100644
index 0000000000..72de66dbff
--- /dev/null
+++ b/.changeset/two-gorillas-drop.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-explore': patch
+---
+
+Updated the example code in the "Customization" section of the README to make the imports match the components used.
diff --git a/.changeset/unlucky-deers-teach.md b/.changeset/unlucky-deers-teach.md
new file mode 100644
index 0000000000..b773e9090f
--- /dev/null
+++ b/.changeset/unlucky-deers-teach.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog-backend-module-github': minor
+---
+
+Implement events support for `GithubMultiOrgEntityProvider`
+
+**BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
diff --git a/.changeset/wet-dolphins-love.md b/.changeset/wet-dolphins-love.md
new file mode 100644
index 0000000000..637d26ca70
--- /dev/null
+++ b/.changeset/wet-dolphins-love.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend-module-github': patch
+---
+
+Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt
index 83fba8aa56..83f05e390d 100644
--- a/.github/vale/Vocab/Backstage/accept.txt
+++ b/.github/vale/Vocab/Backstage/accept.txt
@@ -188,6 +188,7 @@ learnings
Leasot
lerna
Lerna
+lightbox
limitranges
LocalStack
lockdown
@@ -247,6 +248,7 @@ Olausson
Oldsberg
onboarding
Onboarding
+OpenSearch
OpenShift
orgs
padding
@@ -389,6 +391,7 @@ transpilers
truthy
TSDoc
typeahead
+Uffizzi
ui
unbreak
Unconference
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 86ac320ac7..ec0a791dcb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -96,7 +96,7 @@ jobs:
run: node scripts/verify-api-reference.js
- name: verify openapi yaml file matches generated ts file
- run: yarn backstage-repo-tools schema:openapi:verify
+ run: yarn backstage-repo-tools schema openapi verify
- name: verify doc links
run: node scripts/verify-links.js
diff --git a/ADOPTERS.md b/ADOPTERS.md
index b66fbea0f6..7db99119dc 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -132,7 +132,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Tink](https://tink.com/) | [Sebastian Olsson](https://github.com/Sebelino), [Błażej Szum](https://github.com/blazejszumtink), [Anders Eurenius Runvald](https://github.com/anders-er-at-tink) | Internal developer portal which provides templates for creating new Java or Go microservices seamlessly. Also includes a tech radar and a visualization of our CD pipeline. |
| [Brandwatch](https://brandwatch.com) | [Stefan Buck](https://github.com/stefanbuck) | Our primary focus is on the service catalog. Backstage is replacing our homemade service catalog. The switch was quite simple due to the catalog processor API. |
| [Laybuy](https://www.laybuy.com) | [Chris Simmons](https://github.com/contrarianchris) | Backstage is the heart of Laybuy’s new centralised Development Platform, bringing disparate development tools and experiences into a single easy-to-use portal. It simplifies software and API discovery, project scaffolding, and technical documentation, enabling us to embrace golden path development and automate software standards. |
-| [Sendinblue](https://engineering.sendinblue.com/) | [Tanguy Antoine](mailto:antoine.tanguy@sendinblue.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness |
+| [Brevo](https://engineering.brevo.com/) | [Tanguy Antoine](mailto:antoine.tanguy@brevo.com) | Helps us drive the change at scale. Puts light on services, resources, and dependencies. One tool that rules them all through plugins we created for that purpose. We are aiming to put Backstage at the center of every developer's work (Actionable items, Debugging, Monitoring, Provisioning, etc...) to improve their happiness |
| [SafetyCulture](https://safetyculture.com/) | [@R-cen](https://github.com/R-cen), [@lachlancooper](https://github.com/lachlancooper), [@hkf57](https://github.com/hkf57) | Internal developer portal to provide a centralized place for engineers to see an overview of their team's services and information related to the service from other systems. Initially focused on the software catalog, techdocs and search. |
| [Sana Life Science](https://sanalifescience.com) | [Joe Hillyard](mailto:joe@sanalifescience.com) | API Catalog, Tools Management & Control Hub |
| [Ndustrial](https://ndustrial.io) | [Jonathan Skubic](mailto:jonathan@ndustrial.io) | Software Project Catalog |
@@ -243,4 +243,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Chartboost](https://www.chartboost.com)| [@brucearctor](https://github.com/brucearctor), [@ArtemChekunov](https://github.com/ArtemChekunov) | We are building our internal develper portal on top of Backstage.|
| [Quantum Metric](https://www.quantummetric.com/) | [Eric Irwin](https://www.linkedin.com/in/ericirwin1124/) | Backstage is used within our Developer Experience Platform (DXP) in order to increase self-service, standardization and discoverability across our Engineering teams. |
| [VodafoneZiggo](https://www.vodafoneziggo.nl/) | [Peter Macdonald](https://github.com/Parsifal-M) | We use Backstage as our go-to platform for managing our internal tools and services. With Backstage, we can easily discover and access all the services we need to do our work, whether it's deploying code, managing infrastructure, or accessing documentation. We appreciate the standardized, consistent interface and the ability to easily create custom plugins to integrate with our existing workflows. Overall, Backstage is streamlining our internal operations and helps us work more efficiently as a team. |
-| [Volvo Cars](https://www.volvocars.com) | [Martin Wänerskär](https://github.com/martin-wanerskar) | Internal developer portal with intent to unify infrastructure tooling, services, and developer documentation under a single, easy-to-use interface. |
\ No newline at end of file
+| [Volvo Cars](https://www.volvocars.com) | [Martin Wänerskär](https://github.com/martin-wanerskar) | Internal developer portal with intent to unify infrastructure tooling, services, and developer documentation under a single, easy-to-use interface. |
+| [Mercantil Andina](https://www.mercantilandina.com.ar/) | [Sebastian Paredero](https://www.linkedin.com/in/sebastian-paredero/), [Matias Javier Gomez](https://www.linkedin.com/in/matias-javier-gomez-140540a3/), [Gustavo Wilgenhoff](https://github.com/wilgustavo) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. |
+| [Livelo](https://www.livelo.com.br) | [Felipe Nardon](https://www.linkedin.com/in/felipe-nardon-dos-santos/), [Yves Galvão](https://www.linkedin.com/in/yvesgalvao/), [Isaque Rocha](https://www.linkedin.com/in/isaque-rocha/) | Internal platform that aims to reduce the teams' cognitive load through self-service, abstracting processes that are now part of the teams' daily lives, enabling a centralized management of documentation, catalog, metrics and knowledge. |
+| [Worten](https://www.worten.pt) | [@elisiariocouto](https://github.com/elisiariocouto) | We are proud to be an adopter of Backstage. Our goal is to make our Backstage installation the go-to location for every developer. To achieve this, we developed a custom plugin to populate Backstage's catalog with information from our internal deployment tool. As a result, we now have full visibility of all Worten systems, components, APIs, messaging topics, and documentation. We also migrated the internal deployment tool's Web UI to Backstage. Currently, we are working on Backstage's Software Templates feature. |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7828791080..ed1baa133a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -92,10 +92,14 @@ For those contributors who have earned write access to the repository, when a pu
## Code of Conduct
-This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
+This project adheres to the [CNCF Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
[code-of-conduct]: https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md
## Security Issues?
See [SECURITY](SECURITY.md).
+
+## Accessibility
+
+We encourage you to catch any accessibility issues already in the development phase of new features to Backstage, see our [Accessibility documentation](./docs/accessibility/index.md) for more details.
diff --git a/OWNERS.md b/OWNERS.md
index 0a567ee269..25ada5853e 100644
--- a/OWNERS.md
+++ b/OWNERS.md
@@ -77,6 +77,20 @@ Scope: The TechDocs plugin and related tooling
| Morgan Bentell | Spotify | Pulp Fiction | [agentbellnorm](http://github.com/agentbellnorm) | morganbentell#9030 |
| Raghunandan Balachandran | Spotify | Pulp Fiction | [soapraj](http://github.com/soapraj) | raghunandanb#1114 |
+## Incubating Project Areas
+
+These incubating project areas have shared ownership with @backstage/maintainers.
+
+### OpenAPI Tooling
+
+Team: @backstage/openapi-tooling-maintainers
+
+Scope: Tooling for frontend and backend schema-first OpenAPI development.
+
+| Name | Organization | GitHub | Discord |
+| -------------- | ------------ | --------------------------------------- | ------------- |
+| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` |
+
## Sponsors
| Name | Organization | GitHub | Email |
@@ -92,6 +106,7 @@ Scope: The TechDocs plugin and related tooling
| Adam Harvey | Cisco | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey#3739` |
| Andre Wanlin | Keyloop | [awanlin](https://github.com/awanlin) | `Ahhhndre#3095` |
| Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` |
+| Aramis Sennyey | | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` |
| Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` |
| Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` |
| David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` |
diff --git a/README.md b/README.md
index 4952eab10f..5676e262f8 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@

[](https://codecov.io/gh/backstage/backstage)
[](https://github.com/backstage/backstage/releases)
+[](https://app.uffizzi.com/ephemeral-environments/backstage/backstage)
## What is Backstage?
diff --git a/contrib/catalog/ImmediateEntityProvider.ts b/contrib/catalog/ImmediateEntityProvider.ts
index b66c62e330..33ae6697f6 100644
--- a/contrib/catalog/ImmediateEntityProvider.ts
+++ b/contrib/catalog/ImmediateEntityProvider.ts
@@ -9,8 +9,8 @@ import {
DeferredEntity,
EntityProvider,
EntityProviderConnection,
- parseEntityYaml,
-} from '@backstage/plugin-catalog-backend';
+} from '@backstage/plugin-catalog-node';
+import { parseEntityYaml } from '@backstage/plugin-catalog-backend';
import bodyParser from 'body-parser';
import express from 'express';
import Router from 'express-promise-router';
diff --git a/docs/accessibility/index.md b/docs/accessibility/index.md
new file mode 100644
index 0000000000..24685b95ce
--- /dev/null
+++ b/docs/accessibility/index.md
@@ -0,0 +1,60 @@
+---
+id: index
+title: Backstage Accessibility
+description: Documentation on Backstage Accessibility
+---
+
+In an effort to bake accessibility practices further into our process of building features for Backstage, we support automated CI tests for some of our core features in the OSS project: Software Catalog, Software Templates, Search, and TechDocs. As these are just a few of the many plugins out there, we encourage you to consider the accessibility implications of your work on Backstage in order to build a great experience for everyone.
+
+## How to contribute
+
+There are multiple ways to contribute to making Backstage more accessible, you'll find below a list of examples to help you get started.
+
+### Run Lighthouse in CI on your plugin
+
+If your plugin lives in the [Backstage main repository](https://github.com/backstage/backstage/) you can modify the [urls in the Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) to run the Lighthouse checks on urls where your plugin exists as well. E.g.
+
+```diff
+ ci: {
+ collect: {
+ url: [
++ /** Your plugin paths */
++ 'http://localhost:3000/your-plugin-path,
+ /** Software Catalog */
+ 'http://localhost:3000/catalog',
+ 'http://localhost:3000/catalog-import',
+ 'http://localhost:3000/catalog/default/component/backstage',
+ ...
+ ],
+ settings: {
+ ...
+ },
+ ...
+ },
+ assert: {
+ ...
+ },
+ },
+```
+
+To make sure the [Accessibility Github workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) is running when changes are made to your plugin folders, modify the [list of paths](https://github.com/backstage/backstage/blob/10759b6ad2561bd86183ad940256f9a309c7a6b0/.github/workflows/verify_accessibility.yml#L7-L16).
+
+### Run the Lighthouse CLI locally when developing new features
+
+If you want to use the Lighthouse CLI and run the checks based on the config you can use the following command:
+
+```
+yarn dlx @lhci/cli@0.11.x autorun
+```
+
+> Note: running this command will use the [Lighthouse config](https://github.com/backstage/backstage/blob/39ba2284d73885b7ca8290cb38e2b1e4d983c8d6/lighthouserc.js#L19-L34) so make sure to adjust it to your needs if needed.
+
+### Use Lighthouse Github Action on your own repo
+
+If your Backstage plugin lives outside of the [Backstage main repository](https://github.com/backstage/backstage/), and you use GitHub Actions for continuous integration, we encourage you to add and modify the [Accessibility GitHub workflow](https://github.com/backstage/backstage/blob/master/.github/workflows/verify_accessibility.yml) to your needs.
+
+### Report identified issues
+
+It’s important to remember that automated checks can only catch a small number of accessibility issues, therefore we also encourage you to conduct manual testing of your plugins using Assistive technology (screen readers, alternative navigation, and screen magnifiers are a few examples).
+
+If you have identified accessibility issues and don’t have time to contribute a fix, please open an issue over at [Backstage Issues](https://github.com/backstage/backstage/issues) to let us know.
diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md
index dc41617812..ed7fa6aec3 100644
--- a/docs/auth/gitlab/provider.md
+++ b/docs/auth/gitlab/provider.md
@@ -14,15 +14,17 @@ To support GitLab authentication, you must create an Application from the
[GitLab settings](https://gitlab.com/-/profile/applications). The `Redirect URI`
should point to your Backstage backend auth handler.
-Settings for local development:
-
-- Name: Backstage (or your custom app name)
-- Redirect URI: `http://localhost:7007/api/auth/gitlab/handler/frame`
-- Scopes: `read_user` for sign-in. If you also need ID tokens (e.g. if you are
- using the Kubernetes plugin and have clusters with `authProvider: oidc` and
- [`oidcTokenProvider:
-gitlab`](https://backstage.io/docs/features/kubernetes/configuration/#clustersoidctokenprovider-optional)),
- add the `openid` scope.
+1. Set Application Name to `backstage-dev` or something along those lines.
+2. The Authorization Callback URL should match the redirect URI set in Backstage.
+ 1. Set this to `http://localhost:7007/api/auth/gitlab/handler/frame` for local development.
+ 2. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/gitlab/handler/frame` for non-local deployments.
+ 3. Select the following scopes from the list:
+ - [x] `read_user` Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
+ - [x] `read_repository` Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API).
+ - [x] `write_repository` Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API).
+ - [x] `openid` Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships.
+ - [x] `profile` Grants read-only access to the user's profile data using OpenID Connect.
+ - [x] `email` Grants read-only access to the user's primary email address using OpenID Connect.
## Configuration
diff --git a/docs/auth/index.md b/docs/auth/index.md
index abe7fb8db6..b2aac47e2a 100644
--- a/docs/auth/index.md
+++ b/docs/auth/index.md
@@ -348,7 +348,28 @@ const gheAuthApiRef: ApiRef =
});
```
-The new ref is then used to add a new provider to the ApiFactory:
+This new API ref will only work if you define an API factory for it. For example:
+
+```ts
+createApiFactory({
+ api: gheAuthApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
+ GithubAuth.create({
+ configApi,
+ discoveryApi,
+ oauthRequestApi,
+ defaultScopes: ['read:user'],
+ environment: configApi.getOptionalString('auth.environment'),
+ }),
+});
+```
+
+The new API ref is then used to add a new provider to the ApiFactory:
```ts
createApiFactory({
diff --git a/docs/features/kubernetes/authentication.md b/docs/features/kubernetes/authentication.md
index d44d3a4a64..ddeabb5db0 100644
--- a/docs/features/kubernetes/authentication.md
+++ b/docs/features/kubernetes/authentication.md
@@ -66,6 +66,7 @@ as `401` or similar.
The providers available as client side are:
+- `aks`
- `google`
- `oidc`
diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md
index 458637f316..9280c4f020 100644
--- a/docs/features/kubernetes/configuration.md
+++ b/docs/features/kubernetes/configuration.md
@@ -104,12 +104,13 @@ cluster. Valid values are:
| Value | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
-| `google` | This will use a user's Google auth token from the [Google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
+| `aks` | This will use a user's AKS access token from the [Microsoft auth provider](https://backstage.io/docs/auth/microsoft/provider) to access the Kubernetes API on AKS clusters. |
| `aws` | This will use AWS credentials to access resources in EKS clusters |
-| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters |
| `azure` | This will use [Azure Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to access resources in clusters |
+| `google` | This will use a user's Google access token from the [Google auth provider](https://backstage.io/docs/auth/google/provider) to access the Kubernetes API on GKE clusters. |
+| `googleServiceAccount` | This will use the Google Cloud service account credentials to access resources in clusters |
| `oidc` | This will use [Oidc Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) to authenticate to the Kubernetes API. When this is used the `oidcTokenProvider` field should also be set. Please note the cluster must support OIDC, at the time of writing AKS clusters do not support OIDC. |
+| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
Check the [Kubernetes Authentication][4] section for additional explanation.
diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md
index 9fb57cb82f..1c9825d5ca 100644
--- a/docs/features/software-catalog/descriptor-format.md
+++ b/docs/features/software-catalog/descriptor-format.md
@@ -239,17 +239,18 @@ Example: `visits-tracking-service`, `CircleciBuildsDumpV2_avro_gcs`
### `namespace` [optional]
-The ID of a namespace that the entity belongs to. This is a string that follows
-the same format restrictions as `name` above.
+The ID of a namespace that the entity belongs to. This field is optional, and
+currently has no special semantics apart from bounding the name uniqueness
+constraint if specified. It is reserved for future use and may get broader
+semantic implication later.
-This field is optional, and currently has no special semantics apart from
-bounding the name uniqueness constraint if specified. It is reserved for future
-use and may get broader semantic implication later. For now, it is recommended
-to not specify a namespace unless you have specific need to do so. This means
-the entity belongs to the `"default"` namespace.
+For now, it is recommended to not specify a namespace unless you have specific
+need to do so. This means the entity belongs to the `"default"` namespace.
-Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities,
-i.e. not Backstage specific but the same as in Kubernetes.
+Namespaces must be sequences of `[a-zA-Z0-9]`, possibly separated by `-`, at
+most 63 characters in total. Namespace names are case insensitive and will be rendered as lower case in most places.
+
+Example: `tracking-services`, `payment`
### `uid` [output]
diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md
index 15e6a833cc..67bcb6c179 100644
--- a/docs/features/software-templates/writing-templates.md
+++ b/docs/features/software-templates/writing-templates.md
@@ -510,10 +510,8 @@ take a look at, or you can
Each individual step can output some variables that can be used in the
scaffolder frontend for after the job is finished. This is useful for things
-like linking to the entity that has been created with the backend, and also
-linking to the created repository.
-
-The main two that are used are the following:
+like linking to the entity that has been created with the backend, linking
+to the created repository, or showing Markdown text blobs.
```yaml
output:
@@ -523,6 +521,10 @@ output:
- title: Open in catalog
icon: catalog
entityRef: ${{ steps['register'].output.entityRef }} # link to the entity that has been ingested to the catalog
+ text:
+ - title: More information
+ content: |
+ **Entity URL:** `${{ steps['publish'].output.remoteUrl }}`
```
## The templating syntax
diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
index 7398e9a311..820f5ef394 100644
--- a/docs/getting-started/index.md
+++ b/docs/getting-started/index.md
@@ -21,6 +21,9 @@ you to use the [Contributors](contributors.md) guide to do a repository-based in
- Access to a Unix-based operating system, such as Linux, MacOS or
[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)
+- A GNU-like build environment available at the command line.
+ For example, on Debian/Ubuntu you will want to have the `make` and `build-essential` packages installed.
+ On MacOS, you will want to have run `xcode-select --install` to get the XCode command line build tooling in place.
- An account with elevated rights to install the dependencies
- `curl` or `wget` installed
- Node.js [Active LTS Release](https://nodejs.org/en/blog/release/) installed using one of these
diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md
index 821d35db15..32eb2b3de8 100644
--- a/docs/integrations/datadog-rum/installation.md
+++ b/docs/integrations/datadog-rum/installation.md
@@ -24,7 +24,7 @@ app:
# env: 'staging'
```
-If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/master/packages/app/public/index.html#L64) to include the Datadog RUM `init()` section manually.
+If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually.
The `clientToken` and `applicationId` are generated from the Datadog RUM page
following
diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md
index ccffe60bd1..39532e806f 100644
--- a/docs/integrations/github/org.md
+++ b/docs/integrations/github/org.md
@@ -65,6 +65,42 @@ export default async function createPlugin(
}
```
+Alternatively, if you wish to ingest data from multiple GitHub organizations you can use
+the `GithubMultiOrgEntityProvider` instead. Note that by default, this provider will namespace
+groups according to the org they originate from to avoid potential name duplicates:
+
+```ts title="packages/backend/src/plugins/catalog.ts"
+/* highlight-add-next-line */
+import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ const builder = await CatalogBuilder.create(env);
+
+ /* highlight-add-start */
+ // The GitHub URL below needs to match a configured integrations.github entry
+ // specified in your app-config.
+ builder.addEntityProvider(
+ GithubMultiOrgEntityProvider.fromConfig(env.config, {
+ id: 'production',
+ githubUrl: 'https://github.com',
+ // Set the following to list the GitHub orgs you wish to ingest from. You can
+ // also omit this option to ingest all orgs accessible by your GitHub integration
+ orgs: ['org-a', 'org-b'],
+ logger: env.logger,
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 60 },
+ timeout: { minutes: 15 },
+ }),
+ }),
+ );
+ /* highlight-add-end */
+
+ // ..
+}
+```
+
## Installation with Events Support
Please follow the installation instructions at
@@ -110,6 +146,42 @@ export default async function createPlugin(
}
```
+Or, alternatively, if using the `GithubMultiOrgEntityProvider`:
+
+```ts title="packages/backend/src/plugins/catalog.ts"
+/* highlight-add-next-line */
+import { GithubMultiOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github';
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ const builder = await CatalogBuilder.create(env);
+
+ /* highlight-add-start */
+ // The GitHub URL below needs to match a configured integrations.github entry
+ // specified in your app-config.
+ builder.addEntityProvider(
+ GithubMultiOrgEntityProvider.fromConfig(env.config, {
+ id: 'production',
+ githubUrl: 'https://github.com',
+ // Set the following to list the GitHub orgs you wish to ingest from. You can
+ // also omit this option to ingest all orgs accessible by your GitHub integration
+ orgs: ['org-a', 'org-b'],
+ logger: env.logger,
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 60 },
+ timeout: { minutes: 15 },
+ }),
+ // Pass in the eventBroker to allow this provider to subscribe to GitHub events
+ eventBroker: env.eventBroker,
+ }),
+ );
+ /* highlight-add-end */
+
+ // ..
+}
+```
+
You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks).
The webhook will need to be configured to forward `organization`,`team` and `membership` events.
diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md
index c4c057ffe6..66a1855fca 100644
--- a/docs/local-dev/cli-commands.md
+++ b/docs/local-dev/cli-commands.md
@@ -290,7 +290,8 @@ Options:
--package Only load config schema that applies to the given package
--lax Do not require environment variables to be set
--frontend Only validate the frontend configuration
- --deprecated List all deprecated configuration settings
+ --deprecated Output deprecated configuration settings
+ --strict Ensure that the provided config(s) has no errors and does not contain keys not in the schema.
--config Config files to load instead of app-config.yaml (default: [])
-h, --help display help for command
```
diff --git a/docs/overview/support.md b/docs/overview/support.md
index a0444c4d34..c968405a99 100644
--- a/docs/overview/support.md
+++ b/docs/overview/support.md
@@ -15,7 +15,7 @@ description: Support and Community Details and Links
- [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md) -
This is how we roll.
- [Blog](https://backstage.io/blog/) - Announcements and updates.
-- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to
+- [Newsletter](https://info.backstage.spotify.com/newsletter_subscribe) - Subscribe to
our email newsletter.
- Give us a star ⭐️ - If you are using Backstage or think it is an interesting
project, we would love a star! ❤️
diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md
index 6b4ac98646..a4d51b64d0 100644
--- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md
+++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md
@@ -31,11 +31,12 @@ export const todoListUpdatePermission = createPermission({
/* highlight-remove-next-line */
export const todoListPermissions = [todoListCreatePermission];
-/* highlight-add-next-line */
+/* highlight-add-start */
export const todoListPermissions = [
todoListCreatePermission,
todoListUpdatePermission,
];
+/* highlight-add-end */
```
Notice that unlike `todoListCreatePermission`, the `todoListUpdatePermission` permission contains a `resourceType` field. This field indicates to the permission framework that this permission is intended to be authorized in the context of a resource with type `'todo-item'`. You can use whatever string you like as the resource type, as long as you use the same value consistently for each type of resource.
diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md
index 0cae08a668..a9e62040c5 100644
--- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md
+++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md
@@ -66,16 +66,12 @@ export const todoListReadPermission = createPermission({
});
/* highlight-add-end */
-/* highlight-add-next-line */
-export const todoListPermissions = [
- todoListCreatePermission,
- todoListUpdatePermission,
-];
-/* highlight-add-next-line */
export const todoListPermissions = [
todoListCreatePermission,
todoListUpdatePermission,
+ /* highlight-add-start */
todoListReadPermission,
+ /* highlight-add-end */
];
```
diff --git a/docs/publishing.md b/docs/publishing.md
index 142e019fb2..12e4878d5a 100644
--- a/docs/publishing.md
+++ b/docs/publishing.md
@@ -11,7 +11,7 @@ npm.
### Creating a new release
Releases are handled by changesets and trigger whenever the "Version Packages"
-PR is merged. This is typically done every Thursday around noon CET.
+PR is merged. This is typically done every Tuesday around noon CET.
## Emergency Release Process
diff --git a/docs/releases/v1.14.0-next.2-changelog.md b/docs/releases/v1.14.0-next.2-changelog.md
new file mode 100644
index 0000000000..95bbc63953
--- /dev/null
+++ b/docs/releases/v1.14.0-next.2-changelog.md
@@ -0,0 +1,1334 @@
+# Release v1.14.0-next.2
+
+## @backstage/theme@0.3.0-next.0
+
+### Minor Changes
+
+- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+
+## @backstage/plugin-azure-devops@0.3.0-next.2
+
+### Minor Changes
+
+- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-badges-backend@0.2.0-next.2
+
+### Minor Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-catalog-backend-module-github@0.3.0-next.2
+
+### Minor Changes
+
+- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider`
+
+ **BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
+
+### Patch Changes
+
+- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-devtools@0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0-next.0
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## @backstage/plugin-devtools-backend@0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0-next.0
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-devtools-common@0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+## @backstage/plugin-events-backend-module-aws-sqs@0.2.0-next.2
+
+### Minor Changes
+
+- 2c5661f3899: Allow endpoint configuration for sqs, enabling use of localstack for testing.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-backend-module-gitlab@0.2.0-next.2
+
+### Minor Changes
+
+- 439e2986be1: Add a new scaffolder action for gitlab to ensure a group exists
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-common@1.3.0-next.0
+
+### Minor Changes
+
+- 82e10a6939c: Add support for Markdown text blob outputs from templates
+
+## @backstage/plugin-scaffolder-react@1.4.0-next.2
+
+### Minor Changes
+
+- 82e10a6939c: Add support for Markdown text blob outputs from templates
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2
+
+### Minor Changes
+
+- 3d72bdb41c7: Upgrade to aws-sdk v3 and include OpenSearch Serverless support
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/integration-aws-node@0.1.2
+
+## @backstage/plugin-search-react@1.6.0-next.2
+
+### Minor Changes
+
+- 1ce7f84b2e8: accepts InputProp property that can override keys from default
+
+### Patch Changes
+
+- adb31096bc2: Fix text-overflow UI issue for Lifecycle spans in SearchFilter checkbox labels.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/app-defaults@1.3.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## @backstage/backend-openapi-utils@0.0.2-next.1
+
+### Patch Changes
+
+- fe16bd39e83: Use permalinks for links including a line number reference
+- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas.
+
+## @backstage/core-components@0.13.1-next.1
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/create-app@0.5.1-next.2
+
+### Patch Changes
+
+- 1d5e42655cd: Correct command to create new plugins
+
+## @backstage/dev-utils@1.0.15-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/integration-react@1.1.13-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/test-utils@1.3.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## @backstage/plugin-adr@0.5.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-airbrake@0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/dev-utils@1.0.15-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-allure@0.1.34-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-analytics-module-ga@0.1.29-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-analytics-module-ga4@0.1.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-apache-airflow@0.2.11-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-api-docs@0.9.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-apollo-explorer@0.1.11-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-auth-backend@0.18.3-next.2
+
+### Patch Changes
+
+- 3ffcdac7d07: Added a persistent session store through the database
+- Updated dependencies
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-azure-sites@0.1.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-badges@0.2.42-next.2
+
+### Patch Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-bazaar@0.2.8-next.2
+
+### Patch Changes
+
+- 900880ab7c3: Fixed `validateDOMNesting` warnings
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-bitrise@0.1.45-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-catalog@1.11.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-catalog-backend@1.9.1-next.2
+
+### Patch Changes
+
+- ce8d203235b: Ensure that entity cache state is only written to the database when actually changed
+- 485a6c5f7b5: Internal refactoring for performance in the service handlers
+- 3587a968dcd: Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests.
+- 12a345317ab: Remove unnecessary join in the entity facets endpoint, exclude nulls
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-catalog-backend-module-aws@0.1.19-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-kubernetes-common@0.6.3-next.0
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.3.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-catalog-backend-module-openapi@0.1.11-next.2
+
+### Patch Changes
+
+- accaceadffa: Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-catalog-backend-module-puppetdb@0.1.2-next.2
+
+### Patch Changes
+
+- 95b2168d71b: Fixes import paths and updates documentation
+- Updated dependencies
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-catalog-graph@0.2.30-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-catalog-import@0.9.8-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-catalog-react@1.6.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## @backstage/plugin-cicd-statistics@0.1.20-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-cicd-statistics-module-gitlab@0.1.14-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-cicd-statistics@0.1.20-next.2
+
+## @backstage/plugin-circleci@0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-cloudbuild@0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-code-climate@0.1.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-code-coverage@0.2.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-codescene@0.1.13-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-config-schema@0.1.41-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-cost-insights@0.12.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-dynatrace@5.0.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-entity-feedback@0.2.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-entity-validation@0.1.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-explore@0.4.3-next.2
+
+### Patch Changes
+
+- 4851581deb6: Display the title of the entity on the explore card if present, otherwise stick to the name
+- a6025e25d99: Updated the example code in the "Customization" section of the README to make the imports match the components used.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/plugin-explore-react@0.0.28
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-firehydrant@0.2.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-fossa@0.2.50-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-gcalendar@0.3.14-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-gcp-projects@0.3.37-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-git-release-manager@0.3.31-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-github-actions@0.5.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-github-deployments@0.1.49-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-github-issues@0.2.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-github-pull-requests-board@0.1.12-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-gitops-profiles@0.3.36-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-gocd@0.1.24-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-graphiql@0.2.50-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-graphql-voyager@0.1.3-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-home@0.5.2-next.2
+
+### Patch Changes
+
+- acca8966465: Remove object-hash dependency
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-ilert@0.2.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-jenkins@0.7.17-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-kafka@0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-kubernetes@0.9.0-next.2
+
+### Patch Changes
+
+- c7bad1005ba: The Kubernetes plugin now requests AKS access tokens from Azure when retrieving
+ objects from clusters configured with `authProvider: aks` and sets `auth.aks` in
+ its request bodies appropriately.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-kubernetes-common@0.6.3-next.0
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-kubernetes-backend@0.11.0-next.2
+
+### Patch Changes
+
+- 05f1d74539d: Kubernetes clusters now support `authProvider: aks`. When configured this way,
+ the `retrieveObjectsByServiceId` action will use the `auth.aks` value in the
+ request body as a bearer token to authenticate with Kubernetes.
+- Updated dependencies
+ - @backstage/plugin-kubernetes-common@0.6.3-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration-aws-node@0.1.2
+
+## @backstage/plugin-kubernetes-common@0.6.3-next.0
+
+### Patch Changes
+
+- 05f1d74539d: AKS access tokens can now be sent over the wire to the Kubernetes backend.
+
+## @backstage/plugin-lighthouse@0.4.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-linguist@0.1.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-microsoft-calendar@0.1.3-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-newrelic@0.3.36-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-newrelic-dashboard@0.2.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-octopus-deploy@0.2.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-org@0.6.8-next.2
+
+### Patch Changes
+
+- 6e387c077a4: Changed the MembersListCard component to allow displaying aggregated members when viewing a group. Now, a toggle switch can be displayed that lets you switch between showing direct members and aggregated members.
+
+ To enable this new feature, set the showAggregateMembersToggle prop on EntityMembersListCard:
+
+ ```jsx
+ // In packages/app/src/components/catalog/EntityPage.tsx
+ const groupPage = (
+ // ...
+
+ // ...
+ );
+ ```
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-org-react@0.1.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-pagerduty@0.5.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-periskop@0.1.16-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-playlist@0.1.9-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## @backstage/plugin-puppetdb@0.1.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-rollbar@0.4.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-scaffolder@1.13.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+ - @backstage/plugin-scaffolder-react@1.4.0-next.2
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## @backstage/plugin-scaffolder-backend@1.13.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.1.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.21-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-backend-module-sentry@0.1.5-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.3-next.2
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-scaffolder-node@0.1.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+
+## @backstage/plugin-search@1.3.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-search-backend@1.3.1-next.2
+
+### Patch Changes
+
+- 021cfbb5152: Added an OpenAPI 3.0 spec and enforced schema-first model on the router.
+- Updated dependencies
+ - @backstage/config@1.0.7
+
+## @backstage/plugin-sentry@0.5.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-shortcuts@0.3.10-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-sonarqube@0.6.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-splunk-on-call@0.4.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-stack-overflow@0.1.15-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-home@0.5.2-next.2
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-stackstorm@0.1.2-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-tech-insights@0.3.10-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-tech-radar@0.6.4-next.2
+
+### Patch Changes
+
+- be4fa53fab8: Fix description links when clicking entry in radar.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-techdocs@1.6.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-techdocs-addons-test-utils@1.0.13-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-techdocs-react@1.1.6-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-todo@0.2.20-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-user-settings@0.7.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-vault@0.1.12-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
+## @backstage/plugin-xcmetrics@0.2.38-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## example-app@0.2.83-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools@0.1.0-next.0
+ - @backstage/plugin-kubernetes@0.9.0-next.2
+ - @backstage/plugin-home@0.5.2-next.2
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/plugin-badges@0.2.42-next.2
+ - @backstage/plugin-tech-radar@0.6.4-next.2
+ - @backstage/plugin-scaffolder-react@1.4.0-next.2
+ - @backstage/plugin-explore@0.4.3-next.2
+ - @backstage/plugin-org@0.6.8-next.2
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-azure-devops@0.3.0-next.2
+ - @backstage/plugin-stack-overflow@0.1.15-next.2
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-airbrake@0.3.18-next.2
+ - @backstage/plugin-api-docs@0.9.3-next.2
+ - @backstage/plugin-azure-sites@0.1.7-next.2
+ - @backstage/plugin-catalog-graph@0.2.30-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/plugin-circleci@0.3.18-next.2
+ - @backstage/plugin-cloudbuild@0.3.18-next.2
+ - @backstage/plugin-code-coverage@0.2.11-next.2
+ - @backstage/plugin-cost-insights@0.12.7-next.2
+ - @backstage/plugin-dynatrace@5.0.0-next.2
+ - @backstage/plugin-entity-feedback@0.2.1-next.2
+ - @backstage/plugin-gcalendar@0.3.14-next.1
+ - @backstage/plugin-gcp-projects@0.3.37-next.1
+ - @backstage/plugin-github-actions@0.5.18-next.2
+ - @backstage/plugin-gocd@0.1.24-next.2
+ - @backstage/plugin-graphiql@0.2.50-next.1
+ - @backstage/plugin-jenkins@0.7.17-next.2
+ - @backstage/plugin-kafka@0.3.18-next.2
+ - @backstage/plugin-lighthouse@0.4.3-next.2
+ - @backstage/plugin-linguist@0.1.3-next.2
+ - @backstage/plugin-microsoft-calendar@0.1.3-next.1
+ - @backstage/plugin-newrelic@0.3.36-next.1
+ - @backstage/plugin-octopus-deploy@0.2.0-next.2
+ - @backstage/plugin-pagerduty@0.5.11-next.2
+ - @backstage/plugin-playlist@0.1.9-next.2
+ - @backstage/plugin-puppetdb@0.1.1-next.2
+ - @backstage/plugin-rollbar@0.4.18-next.2
+ - @backstage/plugin-scaffolder@1.13.1-next.2
+ - @backstage/plugin-search@1.3.0-next.2
+ - @backstage/plugin-sentry@0.5.3-next.2
+ - @backstage/plugin-shortcuts@0.3.10-next.2
+ - @backstage/plugin-stackstorm@0.1.2-next.1
+ - @backstage/plugin-tech-insights@0.3.10-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/plugin-todo@0.2.20-next.2
+ - @backstage/plugin-user-settings@0.7.3-next.2
+ - @backstage/plugin-apache-airflow@0.2.11-next.1
+ - @backstage/plugin-catalog-import@0.9.8-next.2
+ - @backstage/plugin-newrelic-dashboard@0.2.11-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+ - @internal/plugin-catalog-customized@0.0.10-next.2
+
+## example-backend@0.2.83-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-backend@0.1.0-next.0
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-badges-backend@0.2.0-next.2
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.0-next.2
+ - @backstage/plugin-auth-backend@0.18.3-next.2
+ - @backstage/plugin-search-backend@1.3.1-next.2
+ - example-app@0.2.83-next.2
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2
+ - @backstage/config@1.0.7
+
+## example-backend-next@0.0.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.0-next.2
+ - @backstage/plugin-search-backend@1.3.1-next.2
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+
+## e2e-test@0.2.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.1-next.2
+
+## techdocs-cli-embedded-app@0.2.82-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
+## @internal/plugin-catalog-customized@0.0.10-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+
+## @internal/plugin-todo-list@1.0.13-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
diff --git a/microsite/data/plugins/aws-app-development.yaml b/microsite/data/plugins/aws-app-development.yaml
new file mode 100644
index 0000000000..3944bbb4e0
--- /dev/null
+++ b/microsite/data/plugins/aws-app-development.yaml
@@ -0,0 +1,10 @@
+---
+title: AWS App Developer Tools
+author: Amazon Web Services
+authorUrl: https://aws.amazon.com/
+category: Infrastructure
+description: Create and manage AWS Apps within Backstage
+documentation: https://github.com/awslabs/app-development-for-backstage-io-on-aws#readme
+iconUrl: https://github.com/awslabs/app-development-for-backstage-io-on-aws/blob/main/docs/images/AWS_logo.png?raw=true
+npmPackageName: '@aws/plugin-aws-apps-for-backstage'
+addedDate: '2023-05-10'
diff --git a/microsite/data/plugins/catalog-backend-module-puppetdb.yaml b/microsite/data/plugins/catalog-backend-module-puppetdb.yaml
index 3eac117e19..e7a15ecd4a 100644
--- a/microsite/data/plugins/catalog-backend-module-puppetdb.yaml
+++ b/microsite/data/plugins/catalog-backend-module-puppetdb.yaml
@@ -5,7 +5,7 @@ authorUrl: https://github.com/tdabasinskas
category: Configuration Management
description: Import nodes from PuppetDB into Backstage as Resource Entities
documentation: https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-puppetdb/README.md
-iconUrl: https://avatars.githubusercontent.com/u/234268?s=200&v=4
+iconUrl: /img/puppet.png
npmPackageName: '@backstage/plugin-catalog-backend-module-puppetdb'
tags:
- puppet
diff --git a/microsite/data/plugins/dependencytrack b/microsite/data/plugins/dependencytrack.yaml
similarity index 100%
rename from microsite/data/plugins/dependencytrack
rename to microsite/data/plugins/dependencytrack.yaml
diff --git a/microsite/data/plugins/kubernetes-provider.yaml b/microsite/data/plugins/kubernetes-provider.yaml
new file mode 100644
index 0000000000..e0971d55be
--- /dev/null
+++ b/microsite/data/plugins/kubernetes-provider.yaml
@@ -0,0 +1,10 @@
+---
+title: Kubernetes Entity Provider
+author: Antoine Dao
+authorUrl: https://github.com/antoinedao
+category: Kubernetes
+description: Import Kubernetes resources into Backstage Components
+documentation: https://github.com/AntoineDao/backstage-provider-kubernetes#readme
+iconUrl: https://avatars.githubusercontent.com/u/13629408
+npmPackageName: https://www.npmjs.com/package/@antoinedao/backstage-provider-kubernetes
+addedDate: '2023-04-10'
diff --git a/microsite/data/plugins/xkcd.yaml b/microsite/data/plugins/xkcd.yaml
new file mode 100644
index 0000000000..370718a08f
--- /dev/null
+++ b/microsite/data/plugins/xkcd.yaml
@@ -0,0 +1,10 @@
+---
+title: xkcd comic
+author: Vity
+authorUrl: https://github.com/Vity01
+category: Humor
+description: xkcd comic within Backstage
+documentation: https://github.com/Vity01/backstage-xkcd/
+iconUrl: https://raw.githubusercontent.com/Vity01/backstage-xkcd/main/docs/xkcd.svg
+npmPackageName: 'backstage-plugin-xkcd'
+addedDate: '2023-05-05'
diff --git a/microsite/docusaurus.config.js b/microsite/docusaurus.config.js
index e6137b572b..d0f6bb277f 100644
--- a/microsite/docusaurus.config.js
+++ b/microsite/docusaurus.config.js
@@ -241,7 +241,7 @@ module.exports = {
},
{
label: 'Subscribe to our newsletter',
- to: 'https://mailchi.mp/spotify/backstage-community',
+ to: 'https://info.backstage.spotify.com/newsletter_subscribe',
},
{
label: 'CNCF Incubation',
diff --git a/microsite/package.json b/microsite/package.json
index 6e20cad4fa..601a234688 100644
--- a/microsite/package.json
+++ b/microsite/package.json
@@ -17,7 +17,7 @@
"docusaurus": "docusaurus"
},
"devDependencies": {
- "@docusaurus/module-type-aliases": "0.0.0-5566",
+ "@docusaurus/module-type-aliases": "0.0.0-5569",
"@spotify/prettier-config": "^14.0.0",
"@tsconfig/docusaurus": "^1.0.6",
"@types/webpack-env": "^1.18.0",
@@ -28,9 +28,9 @@
},
"prettier": "@spotify/prettier-config",
"dependencies": {
- "@docusaurus/core": "0.0.0-5566",
- "@docusaurus/plugin-client-redirects": "0.0.0-5566",
- "@docusaurus/preset-classic": "0.0.0-5566",
+ "@docusaurus/core": "0.0.0-5569",
+ "@docusaurus/plugin-client-redirects": "0.0.0-5569",
+ "@docusaurus/preset-classic": "0.0.0-5569",
"@swc/core": "^1.3.46",
"clsx": "^1.1.1",
"docusaurus-plugin-sass": "^0.2.3",
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index cf2f9b7b56..1515c483b9 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -408,6 +408,7 @@
"label": "Core Services",
"items": ["backend-system/core-services/index"]
}
- ]
+ ],
+ "Accessibility": ["accessibility/index"]
}
}
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 10028fc4b4..c16f65240e 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -1620,9 +1620,9 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/core@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/core@npm:0.0.0-5566"
+"@docusaurus/core@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/core@npm:0.0.0-5569"
dependencies:
"@babel/core": ^7.20.12
"@babel/generator": ^7.21.1
@@ -1634,13 +1634,13 @@ __metadata:
"@babel/runtime": ^7.21.0
"@babel/runtime-corejs3": ^7.21.0
"@babel/traverse": ^7.21.2
- "@docusaurus/cssnano-preset": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
+ "@docusaurus/cssnano-preset": 0.0.0-5569
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/react-loadable": 5.5.2
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-common": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
"@slorber/static-site-generator-webpack-plugin": ^4.0.7
"@svgr/webpack": ^6.5.1
autoprefixer: ^10.4.13
@@ -1700,41 +1700,41 @@ __metadata:
react-dom: ^16.8.4 || ^17.0.0
bin:
docusaurus: bin/docusaurus.mjs
- checksum: c85ea103cd3a14dc340533d1cad0627736d87e4336b5919adabd23627958eed108fddb2dd45066944185ebe52a8648a4463a9587804f3a537f0a8deaea3e835e
+ checksum: 573746ba85750381b110a98cfb529d8c961523b735c1945016bf8f79ba2cde38d08ecda88dc1be4852d8c9a49121e23ec964b05354fd6f6d26e6f31e71f175c1
languageName: node
linkType: hard
-"@docusaurus/cssnano-preset@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5566"
+"@docusaurus/cssnano-preset@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5569"
dependencies:
cssnano-preset-advanced: ^5.3.10
postcss: ^8.4.21
postcss-sort-media-queries: ^4.3.0
tslib: ^2.5.0
- checksum: 9aece7f723e7bdeecaa0cc9ce956ef590dfd1c11200f9799236ce80ad94f4653c717a0f16d8694a9f635fb21e0e5bbf3093ba59f8996c012b71062560d9e0fd3
+ checksum: 11408a0f9eb1a4a83b2045a0172ef1fbf0523a99fa9d81a8efe9a3a968b449887616ad8d95b0abb55b2bf82a178193c20c0304942c4f2ba51b693b9209031204
languageName: node
linkType: hard
-"@docusaurus/logger@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/logger@npm:0.0.0-5566"
+"@docusaurus/logger@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/logger@npm:0.0.0-5569"
dependencies:
chalk: ^4.1.2
tslib: ^2.5.0
- checksum: aab288cf6f7c062bb54de296723bbd8deea3029267c49e3f2fb6ab86b462256c4cff805debfd93bd3b35daaefdb9f5bb1a07876434e0b4887e0ec6cf159594d0
+ checksum: 26be75d7180d9b9d525ce5014e1e24c69fd54d1707cabc5acc76b47e3282cfb185613fb4d27f32b5cced92fe80384c68e2d991cf4cabbecca331f570c3e10d9b
languageName: node
linkType: hard
-"@docusaurus/mdx-loader@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/mdx-loader@npm:0.0.0-5566"
+"@docusaurus/mdx-loader@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/mdx-loader@npm:0.0.0-5569"
dependencies:
"@babel/parser": ^7.21.2
"@babel/traverse": ^7.21.2
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
"@mdx-js/mdx": ^2.1.5
escape-html: ^1.0.3
estree-util-value-to-estree: ^2.1.0
@@ -1757,16 +1757,16 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: d4e82a3bdf8eefe7c3996c9ac3860a60f6bb028ab60dce31cef199d22da606631e7eacfeacd4e51c0181d3649026e081c353dea5f01317ec05b48c20b4e8cc39
+ checksum: bcf3b58ea3840d648ddc81c090c9905d559f642d8dc495f3fa86b377ae0bdabb6e1486ae2b258efa459a8bcf1da729a4a3a0c144f0de5dc083155b6d5c83099c
languageName: node
linkType: hard
-"@docusaurus/module-type-aliases@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5566"
+"@docusaurus/module-type-aliases@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5569"
dependencies:
"@docusaurus/react-loadable": 5.5.2
- "@docusaurus/types": 0.0.0-5566
+ "@docusaurus/types": 0.0.0-5569
"@types/history": ^4.7.11
"@types/react": "*"
"@types/react-router-config": "*"
@@ -1776,19 +1776,19 @@ __metadata:
peerDependencies:
react: "*"
react-dom: "*"
- checksum: 6f8a86386c54829f41e28d01a7ffe96b2a3211aaa9fd217f614154726620ce118a0a9bba5563b512bdbc4d3e4a03e1d73848caadbd7fe2b55e59e4ff1ae3633a
+ checksum: 08168317c606e44f8ead775bfc23feaeb3a03178a19a07504130a32cbf189926c08ef40cdc4f00a6df0983383ca0145a42df9112451b15a19067d8b6b10d5f89
languageName: node
linkType: hard
-"@docusaurus/plugin-client-redirects@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5566"
+"@docusaurus/plugin-client-redirects@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-common": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
eta: ^2.0.1
fs-extra: ^11.1.0
lodash: ^4.17.21
@@ -1796,21 +1796,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 01976bc176541b7506680f56f74f2f5cf8d128f33eb6151bdd3bf09795a5ec5161356f4d7711c1fb63e7617fbdd2c6d08a4243870144b04fc1c02b67a98bdcb6
+ checksum: f082956089fc889ea67b89c5c19ad6cd0bc9bb35b663a701f05905786938b1302b8667c2b6d0354bcf9efd35496a5920e1871b488fbc6285d0e36cfe3187e84e
languageName: node
linkType: hard
-"@docusaurus/plugin-content-blog@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5566"
+"@docusaurus/plugin-content-blog@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/mdx-loader": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-common": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
cheerio: ^1.0.0-rc.12
feed: ^4.2.2
fs-extra: ^11.1.0
@@ -1823,21 +1823,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 1d1dfaf61c98de3e5722d1ea726dae29b75eb8747600583347aaca1dfe1ba0cb3b023d1af02ab8aac6e8750288ecd7f5b90bde5e5a693a822c874f0c053a24ea
+ checksum: b9eef1200e1b9bf1cfa7c4d4cb62b7344306d3a096c0cbfc36afd4b973a940b1bf9d6dc62124a0028bbd466313c7ee219444ebe2856588748d2fc33d1f48ae80
languageName: node
linkType: hard
-"@docusaurus/plugin-content-docs@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5566"
+"@docusaurus/plugin-content-docs@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/mdx-loader": 0.0.0-5569
+ "@docusaurus/module-type-aliases": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
"@types/react-router-config": ^5.0.6
combine-promises: ^1.1.0
fs-extra: ^11.1.0
@@ -1850,133 +1850,133 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 32bb9c8c82ff4dfb9639b8fabda4d7b3c69c7a4a551719dba7495be8c7baf2ad96cb79b241904def5bab33f38c4e84a01d870f7200fe5c4eae74d380abaa6c60
+ checksum: 544d51dbb6c9037916aa8e3ed493624029ffdac0d0da70cfdb74f1d431a8da071eeeb0d567be6f63d48f9922f1594f08f950478a4ea427fae8001ff0b4ac5d83
languageName: node
linkType: hard
-"@docusaurus/plugin-content-pages@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5566"
+"@docusaurus/plugin-content-pages@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/mdx-loader": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
fs-extra: ^11.1.0
tslib: ^2.5.0
webpack: ^5.76.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 6764c2d922d308305b0f71866f951b05a9cf885ff758e8982f865d8a52d77295802ae0d35cad20518b45a6a4daf7fbfbe2728fd5e078fdcc5104a4f21de0134f
+ checksum: 16fa680c069d57d9d299ef515487a773c5b89d605cfe5b843392a7bce5e59b539b1855d7e0f2e11878b3021038bff6468b5f4adb9af7dc06884d7207772c2f95
languageName: node
linkType: hard
-"@docusaurus/plugin-debug@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-debug@npm:0.0.0-5566"
+"@docusaurus/plugin-debug@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-debug@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
fs-extra: ^11.1.0
react-json-view: ^1.21.3
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 32a996e40cc58ebc2fd3cbfb9f6d33b7303952788d9c9b2f41f36b54a327939807e990784191f6f3a6169a4c56c7dabec2f7b369828d86f86414e32c3ac39dfe
+ checksum: 0e3b8f5a92bc2b2ab193d76be6174b521238da7b4627775a852a4b4ff8690af28ab4586adbb84fc03abc6da276efc7474eed9350577b457e532defeb638d3184
languageName: node
linkType: hard
-"@docusaurus/plugin-google-analytics@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5566"
+"@docusaurus/plugin-google-analytics@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: a1549324beb15024b3cfc6ebc0fb1ef7017f28d4a7419c14d469cc24025a53e460bceeed7401e01900b530e06ec84c302de891aea165ed5c3885261eb8eccb12
+ checksum: a3e55d3a616009db499ca61493f9cf308c8ae012ec1ff680bff20c3b0b486f8d5d9552fd5c3d7b198e6c72d8e13ace7808d978bd8d56496953d2fd51d5ce0c32
languageName: node
linkType: hard
-"@docusaurus/plugin-google-gtag@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5566"
+"@docusaurus/plugin-google-gtag@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
"@types/gtag.js": ^0.0.12
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 3e98f82b2e2767205fc3d40673e6db5b188d2cd4c3ff83d7f6ff7588b0e2486a6d48c1a6ef37bae6a1fafb77309bbc4000eb2784bd4f01df21deb23235f13884
+ checksum: d0a7f58bea6d5bfd786120ab6c75ab10a4d67bae1d60aadedba649a516f2f9e49237a96a7764f964c79084ae500b9520761cbf6aab60bd5e43bb976f04dd3194
languageName: node
linkType: hard
-"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5566"
+"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 6ea3e845ad5bab9cec34947e851e3adbf27c609227ad4dec8c4bad6ff05ab2190028dea78a4c0d844b6c377357189d9b739ca83c330d8f46dfe25e48ee61037a
+ checksum: acafad7eea5ba06c62addcef4f3cfaa6c43193827ec4b6c2d39ed2799df5301defa8ac8c73d49e35c328bd118c78412745f088764d97eac00da090be8e7906e5
languageName: node
linkType: hard
-"@docusaurus/plugin-sitemap@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5566"
+"@docusaurus/plugin-sitemap@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-common": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
fs-extra: ^11.1.0
sitemap: ^7.1.1
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 1ceb243d048aaadfd05aaa4bb02455d3e87281ba49c08ecaa2ceb58f2cfa94a52af55756e20c562a2fa0c01524396d754d116e6ecb0d7591c2718baeaa334688
+ checksum: 53fd5944a1cfbbe252e3f4398acf8aea3605ca3c5fb9ec48700ce6c162744bae541ccecd1d71e884777dfc2e98c6fa9a01b0f98829e6018e59003c3d4f4a1612
languageName: node
linkType: hard
-"@docusaurus/preset-classic@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/preset-classic@npm:0.0.0-5566"
+"@docusaurus/preset-classic@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/preset-classic@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/plugin-content-blog": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/plugin-content-pages": 0.0.0-5566
- "@docusaurus/plugin-debug": 0.0.0-5566
- "@docusaurus/plugin-google-analytics": 0.0.0-5566
- "@docusaurus/plugin-google-gtag": 0.0.0-5566
- "@docusaurus/plugin-google-tag-manager": 0.0.0-5566
- "@docusaurus/plugin-sitemap": 0.0.0-5566
- "@docusaurus/theme-classic": 0.0.0-5566
- "@docusaurus/theme-common": 0.0.0-5566
- "@docusaurus/theme-search-algolia": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/plugin-content-blog": 0.0.0-5569
+ "@docusaurus/plugin-content-docs": 0.0.0-5569
+ "@docusaurus/plugin-content-pages": 0.0.0-5569
+ "@docusaurus/plugin-debug": 0.0.0-5569
+ "@docusaurus/plugin-google-analytics": 0.0.0-5569
+ "@docusaurus/plugin-google-gtag": 0.0.0-5569
+ "@docusaurus/plugin-google-tag-manager": 0.0.0-5569
+ "@docusaurus/plugin-sitemap": 0.0.0-5569
+ "@docusaurus/theme-classic": 0.0.0-5569
+ "@docusaurus/theme-common": 0.0.0-5569
+ "@docusaurus/theme-search-algolia": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: e7b1181b414f7795c540a4c9fed282ce350eaeee5006a9eb228cb42990d67ec933231f0af2f0329571ea135d87012a6ad606e9adaee61ae105573bbe6433a698
+ checksum: a20013db1bd1c77e6fc2b901326d62f3003b6f6e6191f26aae6968cc2402837c0e84cc54571849d2af811fdae74d9359c0e96775d61b9f3efbbe881898e7f9a3
languageName: node
linkType: hard
@@ -1992,22 +1992,22 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/theme-classic@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-classic@npm:0.0.0-5566"
+"@docusaurus/theme-classic@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/theme-classic@npm:0.0.0-5569"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/plugin-content-blog": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/plugin-content-pages": 0.0.0-5566
- "@docusaurus/theme-common": 0.0.0-5566
- "@docusaurus/theme-translations": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/mdx-loader": 0.0.0-5569
+ "@docusaurus/module-type-aliases": 0.0.0-5569
+ "@docusaurus/plugin-content-blog": 0.0.0-5569
+ "@docusaurus/plugin-content-docs": 0.0.0-5569
+ "@docusaurus/plugin-content-pages": 0.0.0-5569
+ "@docusaurus/theme-common": 0.0.0-5569
+ "@docusaurus/theme-translations": 0.0.0-5569
+ "@docusaurus/types": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-common": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
"@mdx-js/react": ^2.1.5
clsx: ^1.2.1
copy-text-to-clipboard: ^3.0.1
@@ -2024,21 +2024,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 17ba0e0d5f27b0dfd700dd1c9536eae416268abc8d4b308cdf4015b2aff246153b4906e5aa292be58987ccc10b9fe0d0d3182bbba52e370c3116076787009206
+ checksum: ee25082a4abc0d812cc54ee1bea642766f903cfa47d28c6a6efbc53cdaacf67bee594a4745c7ae97fe3ba6ddb136de36dd0c8cda2c310c5b40c9023bee70fcbb
languageName: node
linkType: hard
-"@docusaurus/theme-common@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-common@npm:0.0.0-5566"
+"@docusaurus/theme-common@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/theme-common@npm:0.0.0-5569"
dependencies:
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/plugin-content-blog": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/plugin-content-pages": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
+ "@docusaurus/mdx-loader": 0.0.0-5569
+ "@docusaurus/module-type-aliases": 0.0.0-5569
+ "@docusaurus/plugin-content-blog": 0.0.0-5569
+ "@docusaurus/plugin-content-docs": 0.0.0-5569
+ "@docusaurus/plugin-content-pages": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-common": 0.0.0-5569
"@types/history": ^4.7.11
"@types/react": "*"
"@types/react-router-config": "*"
@@ -2051,22 +2051,22 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 5f2978300b9a01c93d12e6fcd768259cab224207da15f6fd6889cc3e441fda61354e165f084f0353257e9c6d6af5184adb0f03ce7cf009845751657c3d52a00f
+ checksum: 26a2d7dd6fd8b97e67db3bc093fcb9ce6f14d561aeccf94c3334d18a9a2900483dcb0ec4bb28d31e910bea459f20e4445207d5156c9cc02bd72601eb0256e3cc
languageName: node
linkType: hard
-"@docusaurus/theme-search-algolia@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5566"
+"@docusaurus/theme-search-algolia@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5569"
dependencies:
"@docsearch/react": ^3.3.3
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/theme-common": 0.0.0-5566
- "@docusaurus/theme-translations": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/plugin-content-docs": 0.0.0-5569
+ "@docusaurus/theme-common": 0.0.0-5569
+ "@docusaurus/theme-translations": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
+ "@docusaurus/utils-validation": 0.0.0-5569
algoliasearch: ^4.15.0
algoliasearch-helper: ^3.12.0
clsx: ^1.2.1
@@ -2078,23 +2078,23 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 89256f242ccc5d0e1aca92e817413a1e40d18de58fb069e283741b2e5dbc29b267b345fc6d602ad88ac0faf0a003c3a99d549e9bc9027aa79b0e767ad4d7b79c
+ checksum: 16178b7b9278c91eaedbb1629192dc2255939de6f509b721305c4c1795283e0fd4f859b305ce051ec5efdbe71884ceede6239445916b60ebec1858e3b5161d8f
languageName: node
linkType: hard
-"@docusaurus/theme-translations@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-translations@npm:0.0.0-5566"
+"@docusaurus/theme-translations@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/theme-translations@npm:0.0.0-5569"
dependencies:
fs-extra: ^11.1.0
tslib: ^2.5.0
- checksum: e6595e675617669267b48e8cf79cbde8e65f279b8cdf0a3a45cb75f32ec0d2ddd59d0d02ac3f0aaa5328f0a13cd63a41aaff49a2251ce6665bba4abdfba71361
+ checksum: 695f94ddc66d54b649042f0fb0a88b692613d10898ee3306c6c9b52340373493ac9bcceec9263332454d7c9179339517e40b083e949510f9e48ec834bc61fbb3
languageName: node
linkType: hard
-"@docusaurus/types@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/types@npm:0.0.0-5566"
+"@docusaurus/types@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/types@npm:0.0.0-5569"
dependencies:
"@types/history": ^4.7.11
"@types/react": "*"
@@ -2107,13 +2107,13 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: aec430471a0019e650e168320c168f942ce8097022c45fb70e1ec86ba793b146f95a9d3cf434e3d9c985a45cea363f870cc51395dddaf0d0f600496ca6dbf810
+ checksum: 956b7af3a75b3632d1ff330d018353b38b22b3f4af8b39f452fcde33d74c70312dadd3e3bb40e04ecbe02e2462a67bfcbd4ef631462a9c710616c83186930b69
languageName: node
linkType: hard
-"@docusaurus/utils-common@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/utils-common@npm:0.0.0-5566"
+"@docusaurus/utils-common@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/utils-common@npm:0.0.0-5569"
dependencies:
tslib: ^2.5.0
peerDependencies:
@@ -2121,28 +2121,28 @@ __metadata:
peerDependenciesMeta:
"@docusaurus/types":
optional: true
- checksum: 14cc02aac8a59350244ab733f8c662b838aac9ed3bc156a75113cbf1e2bf50d3db2e722313f9b6fe2616dcfafef3abc858f064289a8a0539b97745c571ec3fca
+ checksum: 7d98e31beea34be91de7fdc971033439e412f535617707c79c4d2c0653c769ff4ce7521bd87f038811ccbae3b1d5c0e5d26eab9935ee8bc3515b926cbe175365
languageName: node
linkType: hard
-"@docusaurus/utils-validation@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/utils-validation@npm:0.0.0-5566"
+"@docusaurus/utils-validation@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/utils-validation@npm:0.0.0-5569"
dependencies:
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
+ "@docusaurus/logger": 0.0.0-5569
+ "@docusaurus/utils": 0.0.0-5569
joi: ^17.8.3
js-yaml: ^4.1.0
tslib: ^2.5.0
- checksum: 85a3570134ee26c88571ccb409c08f8ed182257cd72eb564e2a553e0d32a9a0c8bf4daaf1c33c5805e77e47a9fb97d29dfa0a758d8a7224d70ece80e82b583cc
+ checksum: b16730923e0d6fa627ca3e3105c152e814d540badb93f0a2ae1194672a76f8269ed48ed5c8f3259c0e0b9dfc6dc939035e0ac2e675b283ac4d414c642a95c893
languageName: node
linkType: hard
-"@docusaurus/utils@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/utils@npm:0.0.0-5566"
+"@docusaurus/utils@npm:0.0.0-5569":
+ version: 0.0.0-5569
+ resolution: "@docusaurus/utils@npm:0.0.0-5569"
dependencies:
- "@docusaurus/logger": 0.0.0-5566
+ "@docusaurus/logger": 0.0.0-5569
"@svgr/webpack": ^6.5.1
escape-string-regexp: ^4.0.0
file-loader: ^6.2.0
@@ -2163,7 +2163,7 @@ __metadata:
peerDependenciesMeta:
"@docusaurus/types":
optional: true
- checksum: 82f682fdec2711d4608bb84f4a400e4f93bfbd4fde2da4cce37691b91e3e501bd409e7e86bae19bcc3b5dce576edcd33e078b25c28236ed1f15628d814cfe04e
+ checksum: 33e044f9b5328cf9d1b17637fa41a6138c5e18b5f3d73389bbb9f71735a273a9e27660d9a9103766406549edfa0691e2524cc84e6ebc2b0d308991df3bee0dd9
languageName: node
linkType: hard
@@ -3737,10 +3737,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "backstage-microsite@workspace:."
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/plugin-client-redirects": 0.0.0-5566
- "@docusaurus/preset-classic": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5569
+ "@docusaurus/module-type-aliases": 0.0.0-5569
+ "@docusaurus/plugin-client-redirects": 0.0.0-5569
+ "@docusaurus/preset-classic": 0.0.0-5569
"@spotify/prettier-config": ^14.0.0
"@swc/core": ^1.3.46
"@tsconfig/docusaurus": ^1.0.6
diff --git a/package.json b/package.json
index 44778ae926..7e77896821 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"@types/react": "^17",
"@types/react-dom": "^17"
},
- "version": "1.14.0-next.1",
+ "version": "1.14.0-next.2",
"dependencies": {
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3"
diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md
index 5d781b58ca..a4ab5e013c 100644
--- a/packages/app-defaults/CHANGELOG.md
+++ b/packages/app-defaults/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/app-defaults
+## 1.3.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
## 1.3.1-next.1
### Patch Changes
diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json
index 0a8f2e5dc1..f1c806ebe2 100644
--- a/packages/app-defaults/package.json
+++ b/packages/app-defaults/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
- "version": "1.3.1-next.1",
+ "version": "1.3.1-next.2",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index b20fafb995..84f7ab32a1 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,73 @@
# example-app
+## 0.2.83-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools@0.1.0-next.0
+ - @backstage/plugin-kubernetes@0.9.0-next.2
+ - @backstage/plugin-home@0.5.2-next.2
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/plugin-badges@0.2.42-next.2
+ - @backstage/plugin-tech-radar@0.6.4-next.2
+ - @backstage/plugin-scaffolder-react@1.4.0-next.2
+ - @backstage/plugin-explore@0.4.3-next.2
+ - @backstage/plugin-org@0.6.8-next.2
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-azure-devops@0.3.0-next.2
+ - @backstage/plugin-stack-overflow@0.1.15-next.2
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-airbrake@0.3.18-next.2
+ - @backstage/plugin-api-docs@0.9.3-next.2
+ - @backstage/plugin-azure-sites@0.1.7-next.2
+ - @backstage/plugin-catalog-graph@0.2.30-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/plugin-circleci@0.3.18-next.2
+ - @backstage/plugin-cloudbuild@0.3.18-next.2
+ - @backstage/plugin-code-coverage@0.2.11-next.2
+ - @backstage/plugin-cost-insights@0.12.7-next.2
+ - @backstage/plugin-dynatrace@5.0.0-next.2
+ - @backstage/plugin-entity-feedback@0.2.1-next.2
+ - @backstage/plugin-gcalendar@0.3.14-next.1
+ - @backstage/plugin-gcp-projects@0.3.37-next.1
+ - @backstage/plugin-github-actions@0.5.18-next.2
+ - @backstage/plugin-gocd@0.1.24-next.2
+ - @backstage/plugin-graphiql@0.2.50-next.1
+ - @backstage/plugin-jenkins@0.7.17-next.2
+ - @backstage/plugin-kafka@0.3.18-next.2
+ - @backstage/plugin-lighthouse@0.4.3-next.2
+ - @backstage/plugin-linguist@0.1.3-next.2
+ - @backstage/plugin-microsoft-calendar@0.1.3-next.1
+ - @backstage/plugin-newrelic@0.3.36-next.1
+ - @backstage/plugin-octopus-deploy@0.2.0-next.2
+ - @backstage/plugin-pagerduty@0.5.11-next.2
+ - @backstage/plugin-playlist@0.1.9-next.2
+ - @backstage/plugin-puppetdb@0.1.1-next.2
+ - @backstage/plugin-rollbar@0.4.18-next.2
+ - @backstage/plugin-scaffolder@1.13.1-next.2
+ - @backstage/plugin-search@1.3.0-next.2
+ - @backstage/plugin-sentry@0.5.3-next.2
+ - @backstage/plugin-shortcuts@0.3.10-next.2
+ - @backstage/plugin-stackstorm@0.1.2-next.1
+ - @backstage/plugin-tech-insights@0.3.10-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/plugin-todo@0.2.20-next.2
+ - @backstage/plugin-user-settings@0.7.3-next.2
+ - @backstage/plugin-apache-airflow@0.2.11-next.1
+ - @backstage/plugin-catalog-import@0.9.8-next.2
+ - @backstage/plugin-newrelic-dashboard@0.2.11-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+ - @internal/plugin-catalog-customized@0.0.10-next.2
+
## 0.2.83-next.1
### Patch Changes
diff --git a/packages/app/package.json b/packages/app/package.json
index d4941ecd48..56ae2e9c9e 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "example-app",
- "version": "0.2.83-next.1",
+ "version": "0.2.83-next.2",
"private": true,
"backstage": {
"role": "frontend"
@@ -29,6 +29,7 @@
"@backstage/plugin-cloudbuild": "workspace:^",
"@backstage/plugin-code-coverage": "workspace:^",
"@backstage/plugin-cost-insights": "workspace:^",
+ "@backstage/plugin-devtools": "workspace:^",
"@backstage/plugin-dynatrace": "workspace:^",
"@backstage/plugin-entity-feedback": "workspace:^",
"@backstage/plugin-explore": "workspace:^",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 19edd9640d..bc9eac8c64 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -109,6 +109,8 @@ import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
import { StackstormPage } from '@backstage/plugin-stackstorm';
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
+import { DevToolsPage } from '@backstage/plugin-devtools';
+import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
const app = createApp({
apis,
@@ -291,6 +293,9 @@ const routes = (
} />
} />
} />
+ }>
+ {customDevToolsPage}
+
);
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 904fa4a7d4..ffaf72296a 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -52,6 +52,7 @@ import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import { useApp } from '@backstage/core-plugin-api';
+import BuildIcon from '@material-ui/icons/Build';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -176,6 +177,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
to="/settings"
>
+
{children}
diff --git a/packages/app/src/components/devtools/CustomDevToolsPage.tsx b/packages/app/src/components/devtools/CustomDevToolsPage.tsx
new file mode 100644
index 0000000000..dfe70426aa
--- /dev/null
+++ b/packages/app/src/components/devtools/CustomDevToolsPage.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ConfigContent,
+ ExternalDependenciesContent,
+ InfoContent,
+} from '@backstage/plugin-devtools';
+import { DevToolsLayout } from '@backstage/plugin-devtools';
+import React from 'react';
+
+const DevToolsPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const customDevToolsPage = ;
diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx
index 26a7ab60f3..da1d0d425c 100644
--- a/packages/app/src/components/home/HomePage.tsx
+++ b/packages/app/src/components/home/HomePage.tsx
@@ -21,12 +21,15 @@ import {
ClockConfig,
HomePageStarredEntities,
CustomHomepageGrid,
+ HomePageToolkit,
+ HomePageCompanyLogo,
} from '@backstage/plugin-home';
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
import { HomePageCalendar } from '@backstage/plugin-gcalendar';
import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar';
import React from 'react';
+import HomeIcon from '@material-ui/icons/Home';
const clockConfigs: ClockConfig[] = [
{
@@ -55,12 +58,26 @@ const timeFormat: Intl.DateTimeFormatOptions = {
const defaultConfig = [
{
- component: 'HomePageSearchBar',
+ component: 'CompanyLogo',
x: 0,
y: 0,
width: 12,
height: 1,
},
+ {
+ component: 'WelcomeTitle',
+ x: 0,
+ y: 1,
+ width: 12,
+ height: 1,
+ },
+ {
+ component: 'HomePageSearchBar',
+ x: 0,
+ y: 2,
+ width: 12,
+ height: 1,
+ },
];
export const homePage = (
@@ -78,6 +95,17 @@ export const homePage = (
+
+
+ ,
+ },
+ ]}
+ />
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 045a385218..f8175bac29 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -46,10 +46,10 @@
"test:kubernetes": "backstage-cli package test -t KubernetesContainerRunner --no-watch"
},
"dependencies": {
- "@aws-sdk/abort-controller": "^3.208.0",
- "@aws-sdk/client-s3": "^3.208.0",
- "@aws-sdk/credential-providers": "^3.208.0",
- "@aws-sdk/types": "^3.208.0",
+ "@aws-sdk/abort-controller": "^3.310.0",
+ "@aws-sdk/client-s3": "^3.310.0",
+ "@aws-sdk/credential-providers": "^3.310.0",
+ "@aws-sdk/types": "^3.310.0",
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-dev-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
@@ -114,7 +114,7 @@
}
},
"devDependencies": {
- "@aws-sdk/util-stream-node": "^3.208.0",
+ "@aws-sdk/util-stream-node": "^3.310.0",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/archiver": "^5.1.0",
diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md
index 2eea18b4af..e01a00f8d2 100644
--- a/packages/backend-next/CHANGELOG.md
+++ b/packages/backend-next/CHANGELOG.md
@@ -1,5 +1,15 @@
# example-backend-next
+## 0.0.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.0-next.2
+ - @backstage/plugin-search-backend@1.3.1-next.2
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+
## 0.0.11-next.1
### Patch Changes
diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json
index bf913492ac..f69435a838 100644
--- a/packages/backend-next/package.json
+++ b/packages/backend-next/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
- "version": "0.0.11-next.1",
+ "version": "0.0.11-next.2",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md
index 015a54cf45..2d3d5f021d 100644
--- a/packages/backend-openapi-utils/CHANGELOG.md
+++ b/packages/backend-openapi-utils/CHANGELOG.md
@@ -1,5 +1,12 @@
# @backstage/backend-openapi-utils
+## 0.0.2-next.1
+
+### Patch Changes
+
+- fe16bd39e83: Use permalinks for links including a line number reference
+- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas.
+
## 0.0.2-next.0
### Patch Changes
diff --git a/packages/backend-openapi-utils/README.md b/packages/backend-openapi-utils/README.md
index fe22ac2cf2..ba286f94e8 100644
--- a/packages/backend-openapi-utils/README.md
+++ b/packages/backend-openapi-utils/README.md
@@ -8,7 +8,7 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Bas
### Configuration
-1. Run `yarn --cwd backstage-cli package schema:openapi:generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable.
+1. Run `yarn --cwd backstage-cli package schema openapi generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable.
2. In your plugin's `src/service/createRouter.ts`,
@@ -42,4 +42,4 @@ Router() as ApiRouter>
### Runtime validation
-Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
+Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
diff --git a/packages/backend-openapi-utils/api-report.md b/packages/backend-openapi-utils/api-report.md
index 180a7846ed..a4db8ac7bd 100644
--- a/packages/backend-openapi-utils/api-report.md
+++ b/packages/backend-openapi-utils/api-report.md
@@ -333,7 +333,6 @@ declare namespace internal {
ImmutableSchemaObject,
DocParameter,
DocParameters,
- ResolveDocParameterSchema,
ParameterSchema,
MapToSchema,
ParametersSchema,
@@ -412,7 +411,7 @@ type OptionalMap<
type ParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
-> = ResolveDocParameterSchema extends infer R
+> = SchemaRef extends infer R
? R extends ImmutableSchemaObject
? R extends JSONSchema7
? FromSchema
@@ -549,16 +548,6 @@ type RequiredMap<
[P in Exclude, undefined>]: NonNullable;
};
-// @public (undocumented)
-type ResolveDocParameterSchema<
- Doc extends RequiredDoc,
- Schema extends ImmutableParameterObject['schema'],
-> = Schema extends ImmutableReferenceObject
- ? 'parameters' extends ComponentTypes
- ? ComponentRef
- : never
- : Schema;
-
// @public (undocumented)
type Response_2<
Doc extends RequiredDoc,
diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json
index e59da29840..b3926de712 100644
--- a/packages/backend-openapi-utils/package.json
+++ b/packages/backend-openapi-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
- "version": "0.0.2-next.0",
+ "version": "0.0.2-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend-openapi-utils/src/types/params.ts b/packages/backend-openapi-utils/src/types/params.ts
index dce743e6e8..b737450db7 100644
--- a/packages/backend-openapi-utils/src/types/params.ts
+++ b/packages/backend-openapi-utils/src/types/params.ts
@@ -34,6 +34,7 @@ import {
MapDiscriminatedUnion,
PathTemplate,
RequiredDoc,
+ SchemaRef,
} from './common';
import { FromSchema, JSONSchema7 } from 'json-schema-to-ts';
@@ -76,25 +77,13 @@ export type DocParameters<
}
: never;
-/**
- * @public
- */
-export type ResolveDocParameterSchema<
- Doc extends RequiredDoc,
- Schema extends ImmutableParameterObject['schema'],
-> = Schema extends ImmutableReferenceObject
- ? 'parameters' extends ComponentTypes
- ? ComponentRef
- : never
- : Schema;
-
/**
* @public
*/
export type ParameterSchema<
Doc extends RequiredDoc,
Schema extends ImmutableParameterObject['schema'],
-> = ResolveDocParameterSchema extends infer R
+> = SchemaRef extends infer R
? R extends ImmutableSchemaObject
? R extends JSONSchema7
? FromSchema
diff --git a/packages/backend-test-utils/README.md b/packages/backend-test-utils/README.md
index d0f52388bb..f22cb1bd3c 100644
--- a/packages/backend-test-utils/README.md
+++ b/packages/backend-test-utils/README.md
@@ -12,6 +12,26 @@ cd plugins/my-plugin-backend
yarn add --dev @backstage/backend-test-utils
```
+## Environment variables
+
+- `BACKSTAGE_TEST_DISABLE_DOCKER`
+ - Setting the value to `1` disables Docker for tests
+- `CI`
+ - Setting the value to `1` enables long-running tests, including the ones utilizing Docker
+- `BACKSTAGE_TEST_DOCKER_REGISTRY`
+ - Docker registry mirror address where to pull images for tests, for example `mycompany.docker.io/mirror`
+ - See [documentation](https://node.testcontainers.org/configuration/) for information
+ about authentication (`DOCKER_AUTH_CONFIG`)
+
+Connection strings for different databases that are used for testing. The value of the
+string should point to the running instance of the database.
+
+- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING`
+
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts
index 89ec21e7cc..8c1d6f5193 100644
--- a/packages/backend-test-utils/src/database/types.ts
+++ b/packages/backend-test-utils/src/database/types.ts
@@ -16,6 +16,7 @@
import { DatabaseManager } from '@backstage/backend-common';
import { Knex } from 'knex';
+import { getDockerImageForName } from '../util/getDockerImageForName';
/**
* The possible databases to test against.
@@ -48,35 +49,35 @@ export const allDatabases: Record =
POSTGRES_13: {
name: 'Postgres 13.x',
driver: 'pg',
- dockerImageName: 'postgres:13',
+ dockerImageName: getDockerImageForName('postgres:13'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING',
},
POSTGRES_12: {
name: 'Postgres 12.x',
driver: 'pg',
- dockerImageName: 'postgres:12',
+ dockerImageName: getDockerImageForName('postgres:12'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING',
},
POSTGRES_11: {
name: 'Postgres 11.x',
driver: 'pg',
- dockerImageName: 'postgres:11',
+ dockerImageName: getDockerImageForName('postgres:11'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING',
},
POSTGRES_9: {
name: 'Postgres 9.x',
driver: 'pg',
- dockerImageName: 'postgres:9',
+ dockerImageName: getDockerImageForName('postgres:9'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING',
},
MYSQL_8: {
name: 'MySQL 8.x',
driver: 'mysql2',
- dockerImageName: 'mysql:8',
+ dockerImageName: getDockerImageForName('mysql:8'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING',
},
diff --git a/packages/backend-test-utils/src/util/getDockerImageForName.ts b/packages/backend-test-utils/src/util/getDockerImageForName.ts
new file mode 100644
index 0000000000..7869e7b418
--- /dev/null
+++ b/packages/backend-test-utils/src/util/getDockerImageForName.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const getDockerImageForName = (name: string) => {
+ return process.env.BACKSTAGE_TEST_DOCKER_REGISTRY
+ ? `${process.env.BACKSTAGE_TEST_DOCKER_REGISTRY}/${name}`
+ : name;
+};
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index e676df6839..7a4031b654 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,22 @@
# example-backend
+## 0.2.83-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-backend@0.1.0-next.0
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-badges-backend@0.2.0-next.2
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.0-next.2
+ - @backstage/plugin-auth-backend@0.18.3-next.2
+ - @backstage/plugin-search-backend@1.3.1-next.2
+ - example-app@0.2.83-next.2
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2
+ - @backstage/config@1.0.7
+
## 0.2.83-next.1
### Patch Changes
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 900ee150fc..b5a82fd611 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.83-next.1",
+ "version": "0.2.83-next.2",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,6 +42,7 @@
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-code-coverage-backend": "workspace:^",
+ "@backstage/plugin-devtools-backend": "workspace:^",
"@backstage/plugin-entity-feedback-backend": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 593d30bc3b..976ad9a577 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -64,6 +64,7 @@ import playlist from './plugins/playlist';
import adr from './plugins/adr';
import lighthouse from './plugins/lighthouse';
import linguist from './plugins/linguist';
+import devTools from './plugins/devtools';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
@@ -158,8 +159,8 @@ async function main() {
const eventsEnv = useHotMemoize(module, () => createEnv('events'));
const exploreEnv = useHotMemoize(module, () => createEnv('explore'));
const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse'));
-
const linguistEnv = useHotMemoize(module, () => createEnv('linguist'));
+ const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -185,6 +186,7 @@ async function main() {
apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv));
apiRouter.use('/adr', await adr(adrEnv));
apiRouter.use('/linguist', await linguist(linguistEnv));
+ apiRouter.use('/devtools', await devTools(devToolsEnv));
apiRouter.use(notFoundHandler());
await lighthouse(lighthouseEnv);
diff --git a/packages/backend/src/plugins/badges.ts b/packages/backend/src/plugins/badges.ts
index 37529689f3..fa50731809 100644
--- a/packages/backend/src/plugins/badges.ts
+++ b/packages/backend/src/plugins/badges.ts
@@ -28,5 +28,8 @@ export default async function createPlugin(
config: env.config,
discovery: env.discovery,
badgeFactories: createDefaultBadgeFactories(),
+ tokenManager: env.tokenManager,
+ logger: env.logger,
+ identity: env.identity,
});
}
diff --git a/packages/backend/src/plugins/devtools.ts b/packages/backend/src/plugins/devtools.ts
new file mode 100644
index 0000000000..8e1767ddb1
--- /dev/null
+++ b/packages/backend/src/plugins/devtools.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createRouter } from '@backstage/plugin-devtools-backend';
+import { Router } from 'express';
+import type { PluginEnvironment } from '../types';
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ return createRouter({
+ logger: env.logger,
+ config: env.config,
+ permissions: env.permissions,
+ });
+}
diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml
index 2a74425791..0dc1b91cb5 100644
--- a/packages/catalog-model/examples/domains/artists-domain.yaml
+++ b/packages/catalog-model/examples/domains/artists-domain.yaml
@@ -3,6 +3,7 @@ kind: Domain
metadata:
name: artists
description: Everything related to artists
+ title: Artists
links:
- url: http://example.com/domain/artists/
title: Domain Readme
diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md
index bad849d283..39bee2ed05 100644
--- a/packages/cli/cli-report.md
+++ b/packages/cli/cli-report.md
@@ -58,6 +58,7 @@ Options:
--lax
--frontend
--deprecated
+ --strict
--config
-h, --help
```
diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts
index c3d05291e0..c81d82832e 100644
--- a/packages/cli/src/commands/config/validate.ts
+++ b/packages/cli/src/commands/config/validate.ts
@@ -24,5 +24,6 @@ export default async (opts: OptionValues) => {
mockEnv: opts.lax,
fullVisibility: !opts.frontend,
withDeprecatedKeys: opts.deprecated,
+ strict: opts.strict,
});
};
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index ba11a53cfd..3a40839e25 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -345,6 +345,10 @@ export function registerCommands(program: Command) {
.option('--lax', 'Do not require environment variables to be set')
.option('--frontend', 'Only validate the frontend configuration')
.option('--deprecated', 'Output deprecated configuration settings')
+ .option(
+ '--strict',
+ 'Enable strict config validation, forbidding errors and unknown keys',
+ )
.option(...configOption)
.description(
'Validate that the given configuration loads and matches schema',
diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts
index e066a40e7a..00f7818b41 100644
--- a/packages/cli/src/commands/start/startFrontend.ts
+++ b/packages/cli/src/commands/start/startFrontend.ts
@@ -32,6 +32,30 @@ interface StartAppOptions {
configPaths: string[];
}
+function checkReactVersion() {
+ try {
+ // Make sure we're looking at the root of the target repo
+ const reactPkgPath = require.resolve('react/package.json', {
+ paths: [paths.targetRoot],
+ });
+ const reactPkg = require(reactPkgPath);
+ if (reactPkg.version.startsWith('16.')) {
+ console.log(
+ chalk.yellow(
+ `
+⚠️ ⚠️
+⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️
+⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️
+⚠️ ⚠️
+`,
+ ),
+ );
+ }
+ } catch {
+ /* ignored */
+ }
+}
+
export async function startFrontend(options: StartAppOptions) {
if (options.verifyVersions) {
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
@@ -65,6 +89,8 @@ export async function startFrontend(options: StartAppOptions) {
}
}
+ checkReactVersion();
+
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
const config = await loadCliConfig({
args: options.configPaths,
diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts
index b34e1280c0..aaf3fcfcb9 100644
--- a/packages/cli/src/lib/config.ts
+++ b/packages/cli/src/lib/config.ts
@@ -32,6 +32,7 @@ type Options = {
withFilteredKeys?: boolean;
withDeprecatedKeys?: boolean;
fullVisibility?: boolean;
+ strict?: boolean;
};
export async function loadCliConfig(options: Options) {
@@ -70,6 +71,7 @@ export async function loadCliConfig(options: Options) {
dependencies: localPackageNames,
// Include the package.json in the project root if it exists
packagePaths: [paths.resolveTargetRoot('package.json')],
+ noUndeclaredProperties: options.strict,
});
const { appConfigs } = await loadConfig({
@@ -93,6 +95,7 @@ export async function loadCliConfig(options: Options) {
: ['frontend'],
withFilteredKeys: options.withFilteredKeys,
withDeprecatedKeys: options.withDeprecatedKeys,
+ ignoreSchemaErrors: !options.strict,
});
const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs);
diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md
index 1cc353fd4c..8cd469d4d7 100644
--- a/packages/config-loader/api-report.md
+++ b/packages/config-loader/api-report.md
@@ -180,14 +180,17 @@ export function loadConfigSchema(
): Promise;
// @public
-export type LoadConfigSchemaOptions =
+export type LoadConfigSchemaOptions = (
| {
dependencies: string[];
packagePaths?: string[];
}
| {
serialized: JsonObject;
- };
+ }
+) & {
+ noUndeclaredProperties?: boolean;
+};
// @public
export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7;
diff --git a/packages/config-loader/src/schema/compile.ts b/packages/config-loader/src/schema/compile.ts
index d193e7fbf2..f9e1a3bf76 100644
--- a/packages/config-loader/src/schema/compile.ts
+++ b/packages/config-loader/src/schema/compile.ts
@@ -25,6 +25,7 @@ import {
CONFIG_VISIBILITIES,
ConfigVisibility,
} from './types';
+import { SchemaObject } from 'json-schema-traverse';
/**
* This takes a collection of Backstage configuration schemas from various
@@ -35,6 +36,9 @@ import {
*/
export function compileConfigSchemas(
schemas: ConfigSchemaPackageEntry[],
+ options?: {
+ noUndeclaredProperties?: boolean;
+ },
): ValidationFunc {
// The ajv instance below is stateful and doesn't really allow for additional
// output during validation. We work around this by having this extra piece
@@ -102,6 +106,18 @@ export function compileConfigSchemas(
const merged = mergeConfigSchemas(schemas.map(_ => _.value));
+ if (options?.noUndeclaredProperties) {
+ traverse(merged, (schema: SchemaObject) => {
+ /**
+ * The `additionalProperties` key can only be applied to `type: object` in the JSON
+ * schema.
+ */
+ if (schema?.type === 'object') {
+ schema.additionalProperties ||= false;
+ }
+ });
+ }
+
const validate = ajv.compile(merged);
const visibilityBySchemaPath = new Map();
diff --git a/packages/config-loader/src/schema/load.ts b/packages/config-loader/src/schema/load.ts
index 0309f64ea0..1a96305ae0 100644
--- a/packages/config-loader/src/schema/load.ts
+++ b/packages/config-loader/src/schema/load.ts
@@ -32,12 +32,16 @@ import {
* @public
*/
export type LoadConfigSchemaOptions =
- | {
- dependencies: string[];
- packagePaths?: string[];
- }
- | {
- serialized: JsonObject;
+ | (
+ | {
+ dependencies: string[];
+ packagePaths?: string[];
+ }
+ | {
+ serialized: JsonObject;
+ }
+ ) & {
+ noUndeclaredProperties?: boolean;
};
function errorsToError(errors: ValidationError[]): Error {
@@ -77,7 +81,9 @@ export async function loadConfigSchema(
schemas = serialized.schemas as ConfigSchemaPackageEntry[];
}
- const validate = compileConfigSchemas(schemas);
+ const validate = compileConfigSchemas(schemas, {
+ noUndeclaredProperties: options.noUndeclaredProperties,
+ });
return {
process(
diff --git a/packages/config-loader/src/sources/RemoteConfigSource.ts b/packages/config-loader/src/sources/RemoteConfigSource.ts
index ff75060f56..3a8f6bf812 100644
--- a/packages/config-loader/src/sources/RemoteConfigSource.ts
+++ b/packages/config-loader/src/sources/RemoteConfigSource.ts
@@ -149,7 +149,7 @@ export class RemoteConfigSource implements ConfigSource {
signal: signal as import('node-fetch').RequestInit['signal'],
});
if (!res.ok) {
- throw ResponseError.fromResponse(res);
+ throw await ResponseError.fromResponse(res);
}
const content = await res.text();
diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts
index c71799ed98..1c51dd0698 100644
--- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts
@@ -79,6 +79,16 @@ describe('MicrosoftAuth', () => {
'Requested access token with scopes from multiple Azure resources: one-resource, other-resource. Access tokens can only have a single audience.',
);
});
+
+ it('succeeds when requesting multiple scopes for the same resource', async () => {
+ const accessTokenPromise = microsoftAuth.getAccessToken(
+ 'same-resource/one-scope same-resource/other-scope',
+ );
+
+ await expect(accessTokenPromise).resolves.toEqual(
+ 'tokenForOtherResource',
+ );
+ });
});
describe('without a refresh token', () => {
diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts
index fdffb3fc45..c57c65d830 100644
--- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts
@@ -91,10 +91,15 @@ export default class MicrosoftAuth {
}
private static resourceForScopes(scope: string): Promise {
- const audiences = scope
- .split(' ')
- .map(MicrosoftAuth.resourceForScope)
- .filter(aud => aud !== 'openid');
+ const audiences = [
+ ...new Set(
+ scope
+ .split(' ')
+ .map(MicrosoftAuth.resourceForScope)
+ .filter(aud => aud !== 'openid'),
+ ),
+ ];
+
if (audiences.length > 1) {
return Promise.reject(
new Error(
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index 825b793d2c..90f4e174b0 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/core-components
+## 0.13.1-next.1
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.13.1-next.0
### Patch Changes
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index cdb5a6e4a8..ec46c4c6e3 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
- "version": "0.13.1-next.0",
+ "version": "0.13.1-next.1",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx
index be91753ea3..4d58fdfa77 100644
--- a/packages/core-components/src/components/Link/Link.tsx
+++ b/packages/core-components/src/components/Link/Link.tsx
@@ -188,6 +188,9 @@ export const Link = React.forwardRef(
', () => {
expect(input.textContent).toBe('All results');
// Simulate click on input
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
expect(getByText('test 1')).toBeInTheDocument();
const option = getByText('test 1');
diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx
index 1b57e0d4da..dbf5601112 100644
--- a/packages/core-components/src/components/Select/Select.tsx
+++ b/packages/core-components/src/components/Select/Select.tsx
@@ -16,7 +16,6 @@
import Box from '@material-ui/core/Box';
import Checkbox from '@material-ui/core/Checkbox';
import Chip from '@material-ui/core/Chip';
-import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import FormControl from '@material-ui/core/FormControl';
import InputBase from '@material-ui/core/InputBase';
import InputLabel from '@material-ui/core/InputLabel';
@@ -171,7 +170,7 @@ export function SelectComponent(props: SelectProps) {
onChange(event.target.value as SelectedItems);
};
- const handleClick = (event: React.ChangeEvent) => {
+ const handleOpen = (event: React.ChangeEvent) => {
if (disabled) {
event.preventDefault();
return;
@@ -184,7 +183,7 @@ export function SelectComponent(props: SelectProps) {
});
};
- const handleClickAway = () => {
+ const handleClose = () => {
setOpen(false);
};
@@ -196,88 +195,85 @@ export function SelectComponent(props: SelectProps) {
return (
-
-
- {label}
- }
- label={label}
- tabIndex={0}
- renderValue={s =>
- multiple && (value as any[]).length !== 0 ? (
-
- {(s as string[]).map(selectedValue => (
- el.value === selectedValue)?.value}
- label={
- items.find(el => el.value === selectedValue)?.label
- }
- clickable
- onDelete={handleDelete(selectedValue)}
- className={classes.chip}
- />
- ))}
-
- ) : (
-
- {(value as any[]).length === 0
- ? placeholder || ''
- : items.find(el => el.value === s)?.label}
-
- )
- }
- IconComponent={() =>
- !isOpen ? :
- }
- MenuProps={{
- anchorOrigin: {
- vertical: 'bottom',
- horizontal: 'left',
- },
- transformOrigin: {
- vertical: 'top',
- horizontal: 'left',
- },
- getContentAnchorEl: null,
- }}
- >
- {placeholder && !multiple && (
-
- )}
- {native
- ? items &&
- items.map(item => (
-
- ))
- : items &&
- items.map(item => (
-
+
+ {label}
+ }
+ label={label}
+ tabIndex={0}
+ renderValue={s =>
+ multiple && (value as any[]).length !== 0 ? (
+
+ {(s as string[]).map(selectedValue => (
+ el.value === selectedValue)?.value}
+ label={items.find(el => el.value === selectedValue)?.label}
+ clickable
+ onDelete={handleDelete(selectedValue)}
+ className={classes.chip}
+ />
))}
-
-
-
+
+ ) : (
+
+ {(value as any[]).length === 0
+ ? placeholder || ''
+ : items.find(el => el.value === s)?.label}
+
+ )
+ }
+ IconComponent={() =>
+ !isOpen ? :
+ }
+ MenuProps={{
+ anchorOrigin: {
+ vertical: 'bottom',
+ horizontal: 'left',
+ },
+ transformOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ getContentAnchorEl: null,
+ }}
+ >
+ {placeholder && !multiple && (
+
+ )}
+ {native
+ ? items &&
+ items.map(item => (
+
+ ))
+ : items &&
+ items.map(item => (
+
+ ))}
+
+
);
}
diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx
index 848c85eb60..5cea3dd6b6 100644
--- a/packages/core-components/src/components/Table/Table.tsx
+++ b/packages/core-components/src/components/Table/Table.tsx
@@ -105,7 +105,6 @@ const StyledMTableHeader = withStyles(
borderTop: `1px solid ${theme.palette.grey.A100}`,
borderBottom: `1px solid ${theme.palette.grey.A100}`,
// withStyles hasn't a generic overload for theme
- color: (theme as BackstageTheme).palette.textSubtle,
fontWeight: theme.typography.fontWeightBold,
position: 'static',
wordBreak: 'normal',
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index 948c4017a3..16d93b1867 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/create-app
+## 0.5.1-next.2
+
+### Patch Changes
+
+- 1d5e42655cd: Correct command to create new plugins
+
## 0.5.1-next.1
### Patch Changes
@@ -2595,7 +2601,7 @@
fields that clarify a previous grey area such that the intent of the alteration
is made explicit. This is caught by `tsc` and your editor if you are using the
`@ts-check` and `@param` syntax in your migration files
- ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
+ ([example](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
which we strongly recommend.
See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more
@@ -2665,7 +2671,7 @@
fields that clarify a previous grey area such that the intent of the alteration
is made explicit. This is caught by `tsc` and your editor if you are using the
`@ts-check` and `@param` syntax in your migration files
- ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
+ ([example](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
which we strongly recommend.
See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index ef61e1c5b8..841c1b401d 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
- "version": "0.5.1-next.1",
+ "version": "0.5.1-next.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index f00500af68..0682114a35 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -39,10 +39,7 @@
},
"resolutions": {
"@types/react": "^17",
- "@types/react-dom": "^17",
- "@octokit/rest": "18.5.6",
- "@octokit/plugin-rest-endpoint-methods": "5.16.2",
- "@octokit/plugin-paginate-rest": "2.7.0"
+ "@types/react-dom": "^17"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
diff --git a/packages/create-app/templates/default-app/plugins/README.md b/packages/create-app/templates/default-app/plugins/README.md
index 58dc32cb08..d7865fdba3 100644
--- a/packages/create-app/templates/default-app/plugins/README.md
+++ b/packages/create-app/templates/default-app/plugins/README.md
@@ -4,6 +4,6 @@ This is where your own plugins and their associated modules live, each in a
separate folder of its own.
If you want to create a new plugin here, go to your project root directory, run
-the command `yarn backstage-cli create`, and follow the on-screen instructions.
+the command `yarn new`, and follow the on-screen instructions.
You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)!
diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md
index a6aeda0901..77e5241e7e 100644
--- a/packages/dev-utils/CHANGELOG.md
+++ b/packages/dev-utils/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/dev-utils
+## 1.0.15-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 1.0.15-next.1
### Patch Changes
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index ca5057c928..c14732070f 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
- "version": "1.0.15-next.1",
+ "version": "1.0.15-next.2",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md
index 2486a83f5c..eaec405ec9 100644
--- a/packages/e2e-test/CHANGELOG.md
+++ b/packages/e2e-test/CHANGELOG.md
@@ -1,5 +1,12 @@
# e2e-test
+## 0.2.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.1-next.2
+
## 0.2.3-next.1
### Patch Changes
diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json
index 513a9c6a96..60877e8d66 100644
--- a/packages/e2e-test/package.json
+++ b/packages/e2e-test/package.json
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
- "version": "0.2.3-next.1",
+ "version": "0.2.3-next.2",
"private": true,
"backstage": {
"role": "cli"
diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json
index 7afc31cc47..30a95b549e 100644
--- a/packages/integration-aws-node/package.json
+++ b/packages/integration-aws-node/package.json
@@ -32,11 +32,11 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
- "@aws-sdk/client-sts": "^3.208.0",
- "@aws-sdk/credential-provider-node": "^3.208.0",
- "@aws-sdk/credential-providers": "^3.208.0",
- "@aws-sdk/types": "^3.208.0",
- "@aws-sdk/util-arn-parser": "^3.208.0",
+ "@aws-sdk/client-sts": "^3.310.0",
+ "@aws-sdk/credential-provider-node": "^3.310.0",
+ "@aws-sdk/credential-providers": "^3.310.0",
+ "@aws-sdk/types": "^3.310.0",
+ "@aws-sdk/util-arn-parser": "^3.310.0",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^"
},
diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md
index ac62e9aab7..f9c035b7a6 100644
--- a/packages/integration-react/CHANGELOG.md
+++ b/packages/integration-react/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/integration-react
+## 1.1.13-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 1.1.13-next.1
### Patch Changes
diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json
index e59c8f48c4..3124557ebc 100644
--- a/packages/integration-react/package.json
+++ b/packages/integration-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
- "version": "1.1.13-next.1",
+ "version": "1.1.13-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md
index b361bac02f..789b4bc407 100644
--- a/packages/repo-tools/cli-report.md
+++ b/packages/repo-tools/cli-report.md
@@ -14,8 +14,7 @@ Options:
Commands:
api-reports [options] [paths...]
type-deps
- schema:openapi:verify [paths...]
- schema:openapi:generate [paths...]
+ schema [command]
help [command]
```
@@ -37,19 +36,46 @@ Options:
-h, --help
```
-### `backstage-repo-tools schema:openapi:generate`
+### `backstage-repo-tools schema`
```
-Usage: backstage-repo-tools schema:openapi:generate [options] [paths...]
+Usage: backstage-repo-tools schema [options] [command] [command]
+
+Options:
+ -h, --help
+
+Commands:
+ openapi [command]
+ help [command]
+```
+
+### `backstage-repo-tools schema openapi`
+
+```
+Usage: backstage-repo-tools schema openapi [options] [command] [command]
+
+Options:
+ -h, --help
+
+Commands:
+ verify [paths...]
+ generate [paths...]
+ help [command]
+```
+
+### `backstage-repo-tools schema openapi generate`
+
+```
+Usage: backstage-repo-tools schema openapi generate [options] [paths...]
Options:
-h, --help
```
-### `backstage-repo-tools schema:openapi:verify`
+### `backstage-repo-tools schema openapi verify`
```
-Usage: backstage-repo-tools schema:openapi:verify [options] [paths...]
+Usage: backstage-repo-tools schema openapi verify [options] [paths...]
Options:
-h, --help
diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts
index f4d02df3bc..a8324c7d33 100644
--- a/packages/repo-tools/src/commands/index.ts
+++ b/packages/repo-tools/src/commands/index.ts
@@ -18,6 +18,30 @@ import { assertError } from '@backstage/errors';
import { Command } from 'commander';
import { exitWithError } from '../lib/errors';
+function registerSchemaCommand(program: Command) {
+ const command = program
+ .command('schema [command]')
+ .description('Various tools for working with API schema');
+
+ const openApiCommand = command
+ .command('openapi [command]')
+ .description('Tooling for OpenApi schema');
+
+ openApiCommand
+ .command('verify [paths...]')
+ .description(
+ 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.',
+ )
+ .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand)));
+
+ openApiCommand
+ .command('generate [paths...]')
+ .description(
+ 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.',
+ )
+ .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand)));
+}
+
export function registerCommands(program: Command) {
program
.command('api-reports [paths...]')
@@ -63,19 +87,7 @@ export function registerCommands(program: Command) {
.description('Find inconsistencies in types of all packages and plugins')
.action(lazy(() => import('./type-deps/type-deps').then(m => m.default)));
- program
- .command('schema:openapi:verify [paths...]')
- .description(
- 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.',
- )
- .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand)));
-
- program
- .command('schema:openapi:generate [paths...]')
- .description(
- 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.',
- )
- .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand)));
+ registerSchemaCommand(program);
}
// Wraps an action function so that it always exits and handles errors
diff --git a/packages/repo-tools/src/commands/openapi/verify.ts b/packages/repo-tools/src/commands/openapi/verify.ts
index f6dda66738..4c4f310f73 100644
--- a/packages/repo-tools/src/commands/openapi/verify.ts
+++ b/packages/repo-tools/src/commands/openapi/verify.ts
@@ -47,7 +47,7 @@ async function verify(directoryPath: string) {
if (!isEqual(schema.default, yaml)) {
const path = relativePath(cliPaths.targetRoot, directoryPath);
throw new Error(
- `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema:openapi:generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`,
+ `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema openapi generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`,
);
}
}
diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md
index 98bf3668d6..3e7e890e7c 100644
--- a/packages/techdocs-cli-embedded-app/CHANGELOG.md
+++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md
@@ -1,5 +1,23 @@
# techdocs-cli-embedded-app
+## 0.2.82-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.82-next.1
### Patch Changes
diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json
index 01f5f8128c..a645d2dac1 100644
--- a/packages/techdocs-cli-embedded-app/package.json
+++ b/packages/techdocs-cli-embedded-app/package.json
@@ -1,6 +1,6 @@
{
"name": "techdocs-cli-embedded-app",
- "version": "0.2.82-next.1",
+ "version": "0.2.82-next.2",
"private": true,
"backstage": {
"role": "frontend"
diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md
index 9fd73a6840..6804a09762 100644
--- a/packages/test-utils/CHANGELOG.md
+++ b/packages/test-utils/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/test-utils
+## 1.3.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
## 1.3.1-next.1
### Patch Changes
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 80d5a2e620..fa2557c4de 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
- "version": "1.3.1-next.1",
+ "version": "1.3.1-next.2",
"publishConfig": {
"access": "public"
},
diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md
index e2490f346e..1e676394b5 100644
--- a/packages/theme/CHANGELOG.md
+++ b/packages/theme/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/theme
+## 0.3.0-next.0
+
+### Minor Changes
+
+- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+
## 0.2.19
### Patch Changes
diff --git a/packages/theme/package.json b/packages/theme/package.json
index e102c067d7..f0e26f5739 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
- "version": "0.2.19",
+ "version": "0.3.0-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts
index 53f3a3ef2b..c0a9022bd0 100644
--- a/packages/theme/src/baseTheme.ts
+++ b/packages/theme/src/baseTheme.ts
@@ -159,7 +159,6 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
head: {
wordBreak: 'break-word',
overflow: 'hidden',
- color: theme.palette.textSubtle,
fontWeight: 'normal',
lineHeight: '1',
},
@@ -198,14 +197,10 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
'&:hover': {
color: 'inherit',
},
- '&:focus': {
- color: 'inherit',
- },
},
// Bold font for highlighting selected column
active: {
fontWeight: 'bold',
- color: 'inherit',
},
},
MuiListItemText: {
diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts
index eb2ef7ce41..23438a1c14 100644
--- a/packages/theme/src/themes.ts
+++ b/packages/theme/src/themes.ts
@@ -33,7 +33,7 @@ export const lightTheme = createTheme({
ok: '#1DB954',
warning: '#FF9800',
error: '#E22134',
- running: '#2E77D0',
+ running: '#1F5493',
pending: '#FFED51',
aborted: '#757575',
},
@@ -48,7 +48,7 @@ export const lightTheme = createTheme({
},
},
primary: {
- main: '#2E77D0',
+ main: '#1F5493',
},
banner: {
info: '#2E77D0',
diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md
index b0ce02fa19..ce583e56d2 100644
--- a/plugins/adr/CHANGELOG.md
+++ b/plugins/adr/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-adr
+## 0.5.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.5.1-next.1
### Patch Changes
diff --git a/plugins/adr/package.json b/plugins/adr/package.json
index 3202a8c14e..707f0396cf 100644
--- a/plugins/adr/package.json
+++ b/plugins/adr/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr",
- "version": "0.5.1-next.1",
+ "version": "0.5.1-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx
index d136de4717..da575d884f 100644
--- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx
+++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx
@@ -29,7 +29,7 @@ import {
SupportButton,
WarningPanel,
} from '@backstage/core-components';
-import { useApi, useRouteRef } from '@backstage/core-plugin-api';
+import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import {
AdrFilePathFilterFn,
@@ -170,6 +170,9 @@ export const EntityAdrContent = (props: {
const adrApi = useApi(adrApiRef);
const entityHasAdrs = isAdrAvailable(entity);
+ const config = useApi(configApiRef);
+ const appSupportConfigured = config?.getOptionalConfig('app.support');
+
const { value, loading, error } = useAsync(async () => {
const url = getAdrLocationUrl(entity, scmIntegrations);
return adrApi.listAdrs(url);
@@ -212,7 +215,7 @@ export const EntityAdrContent = (props: {
return (
-
+ {appSupportConfigured && }
{!entityHasAdrs && (
diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md
index 191207e591..15472a973a 100644
--- a/plugins/airbrake/CHANGELOG.md
+++ b/plugins/airbrake/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-airbrake
+## 0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/dev-utils@1.0.15-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.18-next.1
### Patch Changes
diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json
index 6f7f404742..3934c29680 100644
--- a/plugins/airbrake/package.json
+++ b/plugins/airbrake/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake",
- "version": "0.3.18-next.1",
+ "version": "0.3.18-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md
index a20bd80961..e656638fd6 100644
--- a/plugins/allure/CHANGELOG.md
+++ b/plugins/allure/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-allure
+## 0.1.34-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.34-next.1
### Patch Changes
diff --git a/plugins/allure/package.json b/plugins/allure/package.json
index 2a98ae1faf..d8fb27f36c 100644
--- a/plugins/allure/package.json
+++ b/plugins/allure/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-allure",
"description": "A Backstage plugin that integrates with Allure",
- "version": "0.1.34-next.1",
+ "version": "0.1.34-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md
index 4195e02248..ec3122845a 100644
--- a/plugins/analytics-module-ga/CHANGELOG.md
+++ b/plugins/analytics-module-ga/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-analytics-module-ga
+## 0.1.29-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.29-next.0
### Patch Changes
diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json
index 31215bd84f..2b2542ec2d 100644
--- a/plugins/analytics-module-ga/package.json
+++ b/plugins/analytics-module-ga/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga",
- "version": "0.1.29-next.0",
+ "version": "0.1.29-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md
index 59c03fec0f..15e67dd797 100644
--- a/plugins/analytics-module-ga4/CHANGELOG.md
+++ b/plugins/analytics-module-ga4/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-analytics-module-ga4
+## 0.1.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.0-next.1
### Patch Changes
diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json
index 5e27228534..eee914df92 100644
--- a/plugins/analytics-module-ga4/package.json
+++ b/plugins/analytics-module-ga4/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga4",
- "version": "0.1.0-next.1",
+ "version": "0.1.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md
index 368995a369..612a2a3e1d 100644
--- a/plugins/apache-airflow/CHANGELOG.md
+++ b/plugins/apache-airflow/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-apache-airflow
+## 0.2.11-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.11-next.0
### Patch Changes
diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json
index 6d503e972a..cf7cf9553c 100644
--- a/plugins/apache-airflow/package.json
+++ b/plugins/apache-airflow/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
- "version": "0.2.11-next.0",
+ "version": "0.2.11-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md
index 635b1e2dca..f370ab46ee 100644
--- a/plugins/api-docs/CHANGELOG.md
+++ b/plugins/api-docs/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-api-docs
+## 0.9.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.9.3-next.1
### Patch Changes
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 89e65dfcc3..e31e2ec48e 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-api-docs",
"description": "A Backstage plugin that helps represent API entities in the frontend",
- "version": "0.9.3-next.1",
+ "version": "0.9.3-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md
index 174a862d61..38d322a270 100644
--- a/plugins/apollo-explorer/CHANGELOG.md
+++ b/plugins/apollo-explorer/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-apollo-explorer
+## 0.1.11-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.11-next.0
### Patch Changes
diff --git a/plugins/apollo-explorer/README.md b/plugins/apollo-explorer/README.md
index 98f3116966..23ea71e934 100644
--- a/plugins/apollo-explorer/README.md
+++ b/plugins/apollo-explorer/README.md
@@ -38,17 +38,22 @@ yarn --cwd packages/app add @backstage/plugin-apollo-explorer
Then, in `packages/app/src/App.tsx` add the plugin as a `Route`
```typescript
-
+ {/* other routes... */}
+
+ }
/>
- }
-/>
```
Then, in `packages/app/src/components/Root/Root.tsx` add a sidebar item so users can find your beautiful plugin!
diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json
index c1164cf1d6..5117fa632c 100644
--- a/plugins/apollo-explorer/package.json
+++ b/plugins/apollo-explorer/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apollo-explorer",
- "version": "0.1.11-next.0",
+ "version": "0.1.11-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md
index c6ff454a7d..406298f6d2 100644
--- a/plugins/auth-backend/CHANGELOG.md
+++ b/plugins/auth-backend/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-auth-backend
+## 0.18.3-next.2
+
+### Patch Changes
+
+- 3ffcdac7d07: Added a persistent session store through the database
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.18.3-next.1
### Patch Changes
diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts
index 8d76ca8bf5..c6579e08f8 100644
--- a/plugins/auth-backend/config.d.ts
+++ b/plugins/auth-backend/config.d.ts
@@ -59,6 +59,7 @@ export interface Config {
/**
* The available auth-provider options and attributes
+ * @additionalProperties true
*/
providers?: {
google?: {
diff --git a/plugins/auth-backend/migrations/20230428155633_sessions.js b/plugins/auth-backend/migrations/20230428155633_sessions.js
new file mode 100644
index 0000000000..49462ac503
--- /dev/null
+++ b/plugins/auth-backend/migrations/20230428155633_sessions.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ // See https://github.com/gx0r/connect-session-knex
+ // Modeled loosely after https://github.com/gx0r/connect-session-knex/blob/4e0e36a9afbb13c3000a89f5e341f2d2d4339a02/lib/index.js#L114
+ // For simplicity we always make the session a string
+ // Do NOT change around this table or column names; the connect-session-knex library makes assumptions about them
+ await knex.schema.createTable('sessions', table => {
+ table.comment('Session data');
+ table.string('sid').primary().notNullable().comment('ID of the session');
+ table
+ .text('sess', 'longtext')
+ .notNullable()
+ .comment('Session data, JSON serialized');
+ table
+ .timestamp('expired')
+ .notNullable()
+ .comment('The point in time when the session expires');
+ table.index('sid', 'sessions_sid_idx');
+ table.index('expired', 'sessions_expired_idx');
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('sessions', table => {
+ table.dropIndex([], 'sessions_sid_idx');
+ table.dropIndex([], 'sessions_expired_idx');
+ });
+ await knex.schema.dropTable('sessions');
+};
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 61c3fe9a8d..fbaf3c5423 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
- "version": "0.18.3-next.1",
+ "version": "0.18.3-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -44,6 +44,7 @@
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
"compression": "^1.7.4",
+ "connect-session-knex": "^3.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"express": "^4.17.1",
diff --git a/plugins/auth-backend/src/database/AuthDatabase.ts b/plugins/auth-backend/src/database/AuthDatabase.ts
new file mode 100644
index 0000000000..97e4236754
--- /dev/null
+++ b/plugins/auth-backend/src/database/AuthDatabase.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ DatabaseManager,
+ PluginDatabaseManager,
+ resolvePackagePath,
+} from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import { Knex } from 'knex';
+
+const migrationsDir = resolvePackagePath(
+ '@backstage/plugin-auth-backend',
+ 'migrations',
+);
+
+/**
+ * Ensures that a database connection is established exactly once and only when
+ * asked for, and runs migrations.
+ */
+export class AuthDatabase {
+ readonly #database: PluginDatabaseManager;
+ #promise: Promise | undefined;
+
+ static create(database: PluginDatabaseManager): AuthDatabase {
+ return new AuthDatabase(database);
+ }
+
+ /** @internal */
+ static forTesting(): AuthDatabase {
+ const config = new ConfigReader({
+ backend: {
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ },
+ },
+ });
+ const database = DatabaseManager.fromConfig(config).forPlugin('auth');
+ return new AuthDatabase(database);
+ }
+
+ static async runMigrations(knex: Knex): Promise {
+ await knex.migrate.latest({
+ directory: migrationsDir,
+ });
+ }
+
+ private constructor(database: PluginDatabaseManager) {
+ this.#database = database;
+ }
+
+ get(): Promise {
+ this.#promise ??= this.#database.getClient().then(async client => {
+ if (!this.#database.migrations?.skip) {
+ await AuthDatabase.runMigrations(client);
+ }
+ return client;
+ });
+
+ return this.#promise;
+ }
+}
diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts
index da67a2f88f..cf415c7e13 100644
--- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts
+++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts
@@ -14,33 +14,10 @@
* limitations under the License.
*/
-import Knex, { Knex as KnexType } from 'knex';
-import { DatabaseKeyStore } from './DatabaseKeyStore';
import { DateTime } from 'luxon';
-
-function createDatabaseManager(
- client: KnexType,
- skipMigrations: boolean = false,
-) {
- return {
- getClient: async () => client,
- migrations: {
- skip: skipMigrations,
- },
- };
-}
-
-function createDB() {
- const knex = Knex({
- client: 'better-sqlite3',
- connection: ':memory:',
- useNullAsDefault: true,
- });
- knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
- resource.run('PRAGMA foreign_keys = ON', () => {});
- });
- return knex;
-}
+import { AuthDatabase } from '../database/AuthDatabase';
+import { DatabaseKeyStore } from './DatabaseKeyStore';
+import { TestDatabases } from '@backstage/backend-test-utils';
const keyBase = {
use: 'sig',
@@ -48,95 +25,107 @@ const keyBase = {
alg: 'Base64',
} as const;
+jest.setTimeout(60_000);
+
describe('DatabaseKeyStore', () => {
- it('should store a key', async () => {
- const client = createDB();
- const store = await DatabaseKeyStore.create({
- database: createDatabaseManager(client),
- });
-
- const key = {
- kid: '123',
- ...keyBase,
- };
-
- await expect(store.listKeys()).resolves.toEqual({ items: [] });
- await store.addKey(key);
-
- const { items } = await store.listKeys();
- expect(items).toEqual([{ createdAt: expect.anything(), key }]);
- expect(
- Math.abs(
- DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
- ),
- ).toBeLessThan(10);
+ const databases = TestDatabases.create({
+ ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
- it('should remove stored keys', async () => {
- const client = createDB();
- const store = await DatabaseKeyStore.create({
- database: createDatabaseManager(client),
- });
+ it.each(databases.eachSupportedId())(
+ 'should store a key, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+ await AuthDatabase.runMigrations(knex);
- const key1 = { kid: '1', ...keyBase };
- const key2 = { kid: '2', ...keyBase };
- const key3 = { kid: '3', ...keyBase };
+ const store = new DatabaseKeyStore(knex);
- await store.addKey(key1);
- await store.addKey(key2);
- await store.addKey(key3);
+ const key = {
+ kid: '123',
+ ...keyBase,
+ };
- await expect(store.listKeys()).resolves.toEqual({
- items: [
- { key: key1, createdAt: expect.anything() },
- { key: key2, createdAt: expect.anything() },
- { key: key3, createdAt: expect.anything() },
- ],
- });
+ await expect(store.listKeys()).resolves.toEqual({ items: [] });
+ await store.addKey(key);
- store.removeKeys(['1']);
+ const { items } = await store.listKeys();
+ expect(items).toEqual([{ createdAt: expect.anything(), key }]);
+ expect(
+ Math.abs(
+ DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
+ ),
+ ).toBeLessThan(10);
+ },
+ );
- await expect(store.listKeys()).resolves.toEqual({
- items: [
- { key: key2, createdAt: expect.anything() },
- { key: key3, createdAt: expect.anything() },
- ],
- });
+ it.each(databases.eachSupportedId())(
+ 'should remove stored keys, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+ await AuthDatabase.runMigrations(knex);
- store.removeKeys(['1', '2']);
+ const store = new DatabaseKeyStore(knex);
- await expect(store.listKeys()).resolves.toEqual({
- items: [{ key: key3, createdAt: expect.anything() }],
- });
+ const key1 = { kid: '1', ...keyBase };
+ const key2 = { kid: '2', ...keyBase };
+ const key3 = { kid: '3', ...keyBase };
- store.removeKeys([]);
+ await store.addKey(key1);
+ await store.addKey(key2);
+ await store.addKey(key3);
- await expect(store.listKeys()).resolves.toEqual({
- items: [{ key: key3, createdAt: expect.anything() }],
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [
+ { key: key1, createdAt: expect.anything() },
+ { key: key2, createdAt: expect.anything() },
+ { key: key3, createdAt: expect.anything() },
+ ],
+ });
- store.removeKeys(['3', '4']);
+ await store.removeKeys(['1']);
- await expect(store.listKeys()).resolves.toEqual({
- items: [],
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [
+ { key: key2, createdAt: expect.anything() },
+ { key: key3, createdAt: expect.anything() },
+ ],
+ });
- await store.addKey(key1);
- await store.addKey(key2);
- await store.addKey(key3);
+ await store.removeKeys(['1', '2']);
- await expect(store.listKeys()).resolves.toEqual({
- items: [
- { key: key1, createdAt: expect.anything() },
- { key: key2, createdAt: expect.anything() },
- { key: key3, createdAt: expect.anything() },
- ],
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [{ key: key3, createdAt: expect.anything() }],
+ });
- store.removeKeys(['1', '2', '3']);
+ await store.removeKeys([]);
- await expect(store.listKeys()).resolves.toEqual({
- items: [],
- });
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [{ key: key3, createdAt: expect.anything() }],
+ });
+
+ await store.removeKeys(['3', '4']);
+
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [],
+ });
+
+ await store.addKey(key1);
+ await store.addKey(key2);
+ await store.addKey(key3);
+
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [
+ { key: key1, createdAt: expect.anything() },
+ { key: key2, createdAt: expect.anything() },
+ { key: key3, createdAt: expect.anything() },
+ ],
+ });
+
+ await store.removeKeys(['1', '2', '3']);
+
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [],
+ });
+ },
+ );
});
diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
index 8039837469..0559b698e9 100644
--- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
+++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
@@ -14,19 +14,10 @@
* limitations under the License.
*/
-import {
- PluginDatabaseManager,
- resolvePackagePath,
-} from '@backstage/backend-common';
import { Knex } from 'knex';
import { DateTime } from 'luxon';
import { AnyJWK, KeyStore, StoredKey } from './types';
-const migrationsDir = resolvePackagePath(
- '@backstage/plugin-auth-backend',
- 'migrations',
-);
-
const TABLE = 'signing_keys';
type Row = {
@@ -35,10 +26,6 @@ type Row = {
key: string;
};
-type Options = {
- database: PluginDatabaseManager;
-};
-
const parseDate = (date: string | Date) => {
const parsedDate =
typeof date === 'string'
@@ -55,24 +42,7 @@ const parseDate = (date: string | Date) => {
};
export class DatabaseKeyStore implements KeyStore {
- static async create(options: Options): Promise {
- const { database } = options;
- const client = await database.getClient();
-
- if (!database.migrations?.skip) {
- await client.migrate.latest({
- directory: migrationsDir,
- });
- }
-
- return new DatabaseKeyStore(client);
- }
-
- private readonly client: Knex;
-
- private constructor(client: Knex) {
- this.client = client;
- }
+ constructor(private readonly client: Knex) {}
async addKey(key: AnyJWK): Promise {
await this.client(TABLE).insert({
diff --git a/plugins/auth-backend/src/identity/KeyStores.test.ts b/plugins/auth-backend/src/identity/KeyStores.test.ts
index 375db4698b..6c034eb64b 100644
--- a/plugins/auth-backend/src/identity/KeyStores.test.ts
+++ b/plugins/auth-backend/src/identity/KeyStores.test.ts
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-import { DatabaseManager } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
-
-import { MemoryKeyStore } from './MemoryKeyStore';
+import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
import { KeyStores } from './KeyStores';
+import { MemoryKeyStore } from './MemoryKeyStore';
+import { getVoidLogger } from '@backstage/backend-common';
describe('KeyStores', () => {
const defaultConfigOptions = {
@@ -34,7 +34,10 @@ describe('KeyStores', () => {
it('reads auth section from config', async () => {
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
- const keyStore = await KeyStores.fromConfig(defaultConfig);
+ const keyStore = await KeyStores.fromConfig(defaultConfig, {
+ logger: getVoidLogger(),
+ database: AuthDatabase.forTesting(),
+ });
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
@@ -46,18 +49,10 @@ describe('KeyStores', () => {
});
it('can handle without auth config', async () => {
- const config = new ConfigReader({
- backend: {
- database: {
- client: 'better-sqlite3',
- connection: ':memory:',
- },
- },
+ const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
+ logger: getVoidLogger(),
+ database: AuthDatabase.forTesting(),
});
- const database =
- DatabaseManager.fromConfig(config).forPlugin('auth-backend');
- const keyStore = await KeyStores.fromConfig(config, { database });
-
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
});
@@ -82,7 +77,10 @@ describe('KeyStores', () => {
},
};
const config = new ConfigReader(configOptions);
- const keyStore = await KeyStores.fromConfig(config);
+ const keyStore = await KeyStores.fromConfig(config, {
+ logger: getVoidLogger(),
+ database: AuthDatabase.forTesting(),
+ });
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
expect(createSpy).toHaveBeenCalledWith(
diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts
index e29b4b6fc4..4cd8f91842 100644
--- a/plugins/auth-backend/src/identity/KeyStores.ts
+++ b/plugins/auth-backend/src/identity/KeyStores.ts
@@ -14,20 +14,20 @@
* limitations under the License.
*/
-import { Logger } from 'winston';
import { pickBy } from 'lodash';
+import { Logger } from 'winston';
-import { PluginDatabaseManager } from '@backstage/backend-common';
import { Config } from '@backstage/config';
+import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
-import { MemoryKeyStore } from './MemoryKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
+import { MemoryKeyStore } from './MemoryKeyStore';
import { KeyStore } from './types';
type Options = {
- logger?: Logger;
- database?: PluginDatabaseManager;
+ logger: Logger;
+ database: AuthDatabase;
};
export class KeyStores {
@@ -37,23 +37,16 @@ export class KeyStores {
*
* @returns a KeyStore store
*/
- static async fromConfig(
- config: Config,
- options?: Options,
- ): Promise {
- const { logger, database } = options ?? {};
+ static async fromConfig(config: Config, options: Options): Promise {
+ const { logger, database } = options;
const ks = config.getOptionalConfig('auth.keyStore');
const provider = ks?.getOptionalString('provider') ?? 'database';
- logger?.info(`Configuring "${provider}" as KeyStore provider`);
+ logger.info(`Configuring "${provider}" as KeyStore provider`);
if (provider === 'database') {
- if (!database) {
- throw new Error('This KeyStore provider requires a database');
- }
-
- return await DatabaseKeyStore.create({ database });
+ return new DatabaseKeyStore(await database.get());
}
if (provider === 'memory') {
diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts
new file mode 100644
index 0000000000..194bbbf44c
--- /dev/null
+++ b/plugins/auth-backend/src/migrations.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Knex } from 'knex';
+import { TestDatabases } from '@backstage/backend-test-utils';
+import fs from 'fs';
+
+const migrationsDir = `${__dirname}/../migrations`;
+const migrationsFiles = fs.readdirSync(migrationsDir).sort();
+
+async function migrateUpOnce(knex: Knex): Promise {
+ await knex.migrate.up({ directory: migrationsDir });
+}
+
+async function migrateDownOnce(knex: Knex): Promise {
+ await knex.migrate.down({ directory: migrationsDir });
+}
+
+async function migrateUntilBefore(knex: Knex, target: string): Promise {
+ const index = migrationsFiles.indexOf(target);
+ if (index === -1) {
+ throw new Error(`Migration ${target} not found`);
+ }
+ for (let i = 0; i < index; i++) {
+ await migrateUpOnce(knex);
+ }
+}
+
+jest.setTimeout(60_000);
+
+describe('migrations', () => {
+ const databases = TestDatabases.create({
+ ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
+ });
+
+ it.each(databases.eachSupportedId())(
+ '20230428155633_sessions.js, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+
+ await migrateUntilBefore(knex, '20230428155633_sessions.js');
+ await migrateUpOnce(knex);
+
+ // Ensure that large cookies are supported
+ const data = `{"cookie":"${'a'.repeat(100_000)}"}`;
+ await knex
+ .insert({ sid: 'abc', expired: knex.fn.now(), sess: data })
+ .into('sessions');
+ await knex
+ .insert({ sid: 'def', expired: knex.fn.now(), sess: data })
+ .into('sessions');
+
+ await expect(knex('sessions').orderBy('sid', 'asc')).resolves.toEqual([
+ { sid: 'abc', expired: expect.anything(), sess: data },
+ { sid: 'def', expired: expect.anything(), sess: data },
+ ]);
+
+ await migrateDownOnce(knex);
+
+ await knex.destroy();
+ },
+ );
+});
diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts
index 0d8f736550..6313276508 100644
--- a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts
+++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts
@@ -227,7 +227,7 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers {
{ headers },
);
if (!res.ok) {
- throw ResponseError.fromResponse(res);
+ throw await ResponseError.fromResponse(res);
}
const cfIdentity = await res.json();
return cfIdentity as unknown as CloudflareAccessIdentityProfile;
diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts
index 8013777415..2996ff68be 100644
--- a/plugins/auth-backend/src/service/router.ts
+++ b/plugins/auth-backend/src/service/router.ts
@@ -32,9 +32,11 @@ import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { createOidcRouter, TokenFactory, KeyStores } from '../identity';
import session from 'express-session';
+import connectSessionKnex from 'connect-session-knex';
import passport from 'passport';
import { Minimatch } from 'minimatch';
import { CatalogAuthResolverContext } from '../lib/resolvers';
+import { AuthDatabase } from '../database/AuthDatabase';
/** @public */
export type ProviderFactories = { [s: string]: AuthProviderFactory };
@@ -70,7 +72,11 @@ export async function createRouter(
const appUrl = config.getString('app.baseUrl');
const authUrl = await discovery.getExternalBaseUrl('auth');
- const keyStore = await KeyStores.fromConfig(config, { logger, database });
+ const authDb = AuthDatabase.create(database);
+ const keyStore = await KeyStores.fromConfig(config, {
+ logger,
+ database: authDb,
+ });
const keyDurationSeconds = 3600;
const tokenIssuer = new TokenFactory({
@@ -84,14 +90,18 @@ export async function createRouter(
const secret = config.getOptionalString('auth.session.secret');
if (secret) {
router.use(cookieParser(secret));
- // TODO: Configure the server-side session storage. The default MemoryStore is not designed for production
const enforceCookieSSL = authUrl.startsWith('https');
+ const KnexSessionStore = connectSessionKnex(session);
router.use(
session({
secret,
saveUninitialized: false,
resave: false,
cookie: { secure: enforceCookieSSL ? 'auto' : false },
+ store: new KnexSessionStore({
+ createtable: false,
+ knex: await authDb.get(),
+ }),
}),
);
router.use(passport.initialize());
diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md
index a0328bcb22..608f88437c 100644
--- a/plugins/azure-devops/CHANGELOG.md
+++ b/plugins/azure-devops/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-azure-devops
+## 0.3.0-next.2
+
+### Minor Changes
+
+- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.9-next.1
### Patch Changes
diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md
index 5079fa4f16..7eeae09813 100644
--- a/plugins/azure-devops/README.md
+++ b/plugins/azure-devops/README.md
@@ -70,7 +70,7 @@ dev.azure.com/project:
dev.azure.com/build-definition:
```
-In this case `` will be the name of your Team Project and `` will be the name of the Build Definition you would like to see Builds for. If the Build Definition name has spaces in it make sure to put quotes around it
+In this case `` will be the name of your Team Project and `` will be the name of the Build Definition you would like to see Builds for, and it's possible to add more Builds separated by a comma. If the Build Definition name has spaces in it make sure to put quotes around it.
### Azure Pipelines Component
diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json
index 914cc9a4ba..8b3f4a8496 100644
--- a/plugins/azure-devops/package.json
+++ b/plugins/azure-devops/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops",
- "version": "0.2.9-next.1",
+ "version": "0.3.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
index 68d8896538..a202008798 100644
--- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts
+++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
@@ -120,7 +120,23 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
queryString.append('repoName', repoName);
}
if (definitionName) {
- queryString.append('definitionName', definitionName);
+ const definitionNames = definitionName.split(',');
+ if (definitionNames.length > 1) {
+ const buildRuns: BuildRun[] = [];
+ for (const name of definitionNames) {
+ queryString.set('definitionName', name.trim());
+ if (options?.top) {
+ queryString.set('top', options.top.toString());
+ }
+ const urlSegment = `builds/${encodeURIComponent(
+ projectName,
+ )}?${queryString}`;
+ const items = await this.get(urlSegment);
+ buildRuns.push(...items);
+ }
+ return { items: buildRuns };
+ }
+ queryString.append('definitionName', definitionName.trim());
}
if (options?.top) {
queryString.append('top', options.top.toString());
diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md
index 0d44b9a4e4..23152f9760 100644
--- a/plugins/azure-sites-backend/README.md
+++ b/plugins/azure-sites-backend/README.md
@@ -2,7 +2,7 @@
Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries.
-_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
+_Inspired by [roadie.io AWS Lambda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
## Setup
@@ -65,7 +65,7 @@ Here's how to get the backend plugin up and running:
```ts
import azure from './plugins/azure';
- // Removed for clairty...
+ // Removed for clarity...
async function main() {
// ...
@@ -82,4 +82,4 @@ Here's how to get the backend plugin up and running:
4. Now run `yarn start-backend` from the repo root.
-5. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`.
+5. Finally, open `http://localhost:7007/api/azure-sites/health` in a browser, it should return `{"status":"ok"}`.
diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md
index 1eea015834..20703e33a5 100644
--- a/plugins/azure-sites/CHANGELOG.md
+++ b/plugins/azure-sites/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-azure-sites
+## 0.1.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.7-next.1
### Patch Changes
diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json
index 8863e76dae..c3335d6129 100644
--- a/plugins/azure-sites/package.json
+++ b/plugins/azure-sites/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-sites",
- "version": "0.1.7-next.1",
+ "version": "0.1.7-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md
index 50bf55c918..c543efb0a6 100644
--- a/plugins/badges-backend/CHANGELOG.md
+++ b/plugins/badges-backend/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-badges-backend
+## 0.2.0-next.2
+
+### Minor Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.1.39-next.1
### Patch Changes
diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md
index 63e43c6692..ddbb30bc4e 100644
--- a/plugins/badges-backend/README.md
+++ b/plugins/badges-backend/README.md
@@ -36,6 +36,9 @@ export default async function createPlugin(
config: env.config,
discovery: env.discovery,
badgeFactories: createDefaultBadgeFactories(),
+ tokenManager: env.tokenManager,
+ logger: env.logger,
+ identity: env.identity,
});
}
```
@@ -112,11 +115,29 @@ export const createMyCustomBadgeFactories = (): BadgeFactories => ({
});
```
+### Badge obfuscation
+
+When you enable the obfuscation feature, the badges backend will obfuscate the entity names in the badge link. It's useful when you want your badges to be visible to the public, but you don't want to expose the entity names and also to protect your entity names from being enumerated.
+
+To enable the obfuscation you need to activate the `obfuscation` feature in the `app-config.yaml`:
+
+```yaml
+app:
+ badges:
+ obfuscate: true
+```
+
+:warning: **Warning**: The only endpoint to be publicly available is the `/entity/:entityUuid/:badgeId` endpoint. The other endpoints are meant for trusted internal users and should not be publicly exposed.
+
+> Note that you cannot use env vars to set the `obfuscate` value. It must be a boolean value and env vars are always strings.
+
## API
The badges backend api exposes two main endpoints for entity badges. The
`/badges` prefix is arbitrary, and the default for the example backend.
+### If obfuscation is disabled (default or apps.badges.obfuscate: false)
+
- `/badges/entity/:namespace/:kind/:name/badge-specs` List all defined badges
for a particular entity, in json format. See
[BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts)
@@ -126,6 +147,21 @@ The badges backend api exposes two main endpoints for entity badges. The
an SVG image. If the `accept` request header prefers `application/json` the
badge spec as JSON will be returned instead of the image.
+### If obfuscation is enabled (apps.badges.obfuscate: true)
+
+- `/badges/entity/:namespace/:kind/:name/obfuscated` Get the obfuscated `entity url`.
+
+> Note that endpoint have a embedded authMiddleware to authenticate the user requesting this endpoint. _It meant to be called from the frontend plugin._
+
+- `/badges/entity/:entityUuid/:badgeId` Get the entity badge as an SVG image. If
+ the `accept` request header prefers `application/json` the badge spec as JSON
+ will be returned instead of the image.
+
+- `/badge/entity/:entityUuid/badge-specs` List all defined badges for a
+ particular entity, in json format. See
+ [BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts)
+ from the frontend plugin for a type declaration.
+
## Links
- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/badges)
diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md
index 8ca4b561a2..09a02ba44d 100644
--- a/plugins/badges-backend/api-report.md
+++ b/plugins/badges-backend/api-report.md
@@ -7,7 +7,10 @@ import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+import { Logger } from 'winston';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { TokenManager } from '@backstage/backend-common';
// @public (undocumented)
export interface Badge {
@@ -78,6 +81,27 @@ export type BadgeSpec = {
markdown: string;
};
+// @public
+export interface BadgesStore {
+ // (undocumented)
+ getBadgeFromUuid(uuid: string): Promise<
+ | {
+ name: string;
+ namespace: string;
+ kind: string;
+ }
+ | undefined
+ >;
+ // (undocumented)
+ getBadgeUuid(
+ name: string,
+ namespace: string,
+ kind: string,
+ ): Promise<{
+ uuid: string;
+ }>;
+}
+
// @public (undocumented)
export type BadgeStyle = (typeof BADGE_STYLES)[number];
@@ -107,10 +131,18 @@ export interface RouterOptions {
// (undocumented)
badgeFactories?: BadgeFactories;
// (undocumented)
+ badgeStore?: BadgesStore;
+ // (undocumented)
catalog?: CatalogApi;
// (undocumented)
config: Config;
// (undocumented)
discovery: PluginEndpointDiscovery;
+ // (undocumented)
+ identity: IdentityApi;
+ // (undocumented)
+ logger: Logger;
+ // (undocumented)
+ tokenManager: TokenManager;
}
```
diff --git a/plugins/badges-backend/migrations/20230404_init.js b/plugins/badges-backend/migrations/20230404_init.js
new file mode 100644
index 0000000000..3de6bfc248
--- /dev/null
+++ b/plugins/badges-backend/migrations/20230404_init.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+exports.up = async function up(knex) {
+ await knex.schema.createTable('badges', table => {
+ table.string('kind').notNullable();
+ table.string('namespace').notNullable();
+ table.string('name').notNullable();
+ table.string('uuid').unique().notNullable();
+ table.index(['uuid'], 'badges_uuid_index');
+ table.primary(['kind', 'namespace', 'name']);
+ });
+};
+
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('badges', table => {
+ table.dropIndex('', 'badges_uuid_index');
+ });
+ await knex.schema.dropTable('badges');
+};
diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json
index 5f9b5b51a3..84b4d324d7 100644
--- a/plugins/badges-backend/package.json
+++ b/plugins/badges-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-badges-backend",
"description": "A Backstage backend plugin that generates README badges for your entities",
- "version": "0.1.39-next.1",
+ "version": "0.2.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -38,18 +38,25 @@
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
"@types/express": "^4.17.6",
"badge-maker": "^3.3.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
+ "knex": "^2.4.2",
+ "lodash": "^4.17.21",
+ "supertest": "^6.3.3",
+ "uuid": "^9.0.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/catalog-client": "workspace:^",
"@backstage/cli": "workspace:^",
- "@types/supertest": "^2.0.8",
- "supertest": "^6.1.3"
+ "@types/node": "*",
+ "cross-fetch": "^3.1.5"
},
"files": [
"dist"
diff --git a/plugins/badges-backend/src/database/badgeStore.test.ts b/plugins/badges-backend/src/database/badgeStore.test.ts
new file mode 100644
index 0000000000..5a7a82d4b7
--- /dev/null
+++ b/plugins/badges-backend/src/database/badgeStore.test.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DatabaseBadgesStore } from './badgesStore';
+import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
+import { Knex } from 'knex';
+import { Entity } from '@backstage/catalog-model';
+
+describe('DatabaseBadgesStore', () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'test',
+ },
+ };
+
+ const databases = TestDatabases.create();
+
+ async function createDatabaseBadgesStore(databaseId: TestDatabaseId) {
+ const knex = await databases.init(databaseId);
+ return {
+ knex,
+ badgeStore: await DatabaseBadgesStore.create({
+ database: { getClient: async () => knex },
+ }),
+ };
+ }
+
+ describe.each(databases.eachSupportedId())('%p', databaseId => {
+ let knex: Knex;
+ let badgeStore: DatabaseBadgesStore;
+
+ beforeEach(async () => {
+ ({ knex, badgeStore } = await createDatabaseBadgesStore(databaseId));
+ });
+
+ it('createABadge if not existing in DB', async () => {
+ const uuid = await badgeStore.getBadgeUuid(
+ entity.metadata.name,
+ entity.metadata.namespace || 'default',
+ entity.kind,
+ );
+
+ expect(uuid.uuid).toMatch(
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
+ );
+
+ const storedBadge = await badgeStore.getBadgeFromUuid(uuid.uuid);
+ expect(storedBadge?.kind).toEqual(entity.kind);
+ expect(storedBadge?.name).toEqual(entity.metadata.name);
+ expect(storedBadge?.namespace).toEqual(
+ entity.metadata.namespace || 'default',
+ );
+ });
+
+ it('getBadge if badge already exist in DB', async () => {
+ await knex('badges').truncate();
+ await knex('badges').insert([
+ {
+ uuid: 'uuid1',
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ },
+ ]);
+
+ const storedEntity = await badgeStore.getBadgeFromUuid('uuid1');
+
+ expect(storedEntity).toEqual({
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ });
+ });
+
+ it('getBadgeUuid if badge exist in DB', async () => {
+ await knex('badges').truncate();
+ await knex('badges').insert([
+ {
+ uuid: 'uuid1',
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ },
+ ]);
+
+ const storedUuid = await badgeStore.getBadgeUuid(
+ 'test',
+ 'default',
+ 'component',
+ );
+
+ expect(storedUuid).toEqual({ uuid: 'uuid1' });
+ });
+ });
+});
diff --git a/plugins/badges-backend/src/database/badgesStore.ts b/plugins/badges-backend/src/database/badgesStore.ts
new file mode 100644
index 0000000000..e6c6a9e2ad
--- /dev/null
+++ b/plugins/badges-backend/src/database/badgesStore.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ PluginDatabaseManager,
+ resolvePackagePath,
+} from '@backstage/backend-common';
+import { Knex } from 'knex';
+import { isNil } from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
+
+/**
+ * internal
+ * @public
+ */
+export interface BadgesStore {
+ getBadgeUuid(
+ name: string,
+ namespace: string,
+ kind: string,
+ ): Promise<{ uuid: string }>;
+
+ getBadgeFromUuid(
+ uuid: string,
+ ): Promise<{ name: string; namespace: string; kind: string } | undefined>;
+}
+
+const migrationsDir = resolvePackagePath(
+ '@backstage/plugin-badges-backend', // Package name
+ 'migrations', // Migrations directory
+);
+
+/**
+ * DatabaseBadgesStore
+ * @internal
+ */
+export class DatabaseBadgesStore implements BadgesStore {
+ private constructor(private readonly db: Knex) {}
+
+ static async create({
+ database,
+ skipMigrations,
+ }: {
+ database: PluginDatabaseManager;
+ skipMigrations?: boolean;
+ }): Promise {
+ const client = await database.getClient();
+
+ if (!database.migrations?.skip && !skipMigrations) {
+ await client.migrate.latest({
+ directory: migrationsDir,
+ });
+ }
+
+ return new DatabaseBadgesStore(client);
+ }
+
+ async getBadgeFromUuid(
+ uuid: string,
+ ): Promise<{ name: string; namespace: string; kind: string } | undefined> {
+ const result = await this.db('badges')
+ .select('namespace', 'name', 'kind')
+ .where({ uuid: uuid })
+ .first();
+
+ return result;
+ }
+
+ async getBadgeUuid(
+ name: string,
+ namespace: string,
+ kind: string,
+ ): Promise<{ uuid: string }> {
+ const result = await this.db('badges')
+ .select('uuid')
+ .where({ name: name, namespace: namespace, kind: kind })
+ .first();
+
+ let uuid = result?.uuid;
+
+ if (isNil(uuid)) {
+ uuid = uuidv4();
+
+ await this.db('badges')
+ .insert({
+ uuid: uuid,
+ name: name,
+ namespace: namespace,
+ kind: kind,
+ })
+ .onConflict(['name', 'namespace', 'kind'])
+ .ignore();
+ }
+
+ return { uuid };
+ }
+}
diff --git a/plugins/badges-backend/src/index.ts b/plugins/badges-backend/src/index.ts
index a45714cb81..35a061c516 100644
--- a/plugins/badges-backend/src/index.ts
+++ b/plugins/badges-backend/src/index.ts
@@ -24,3 +24,4 @@ export * from './badges';
export * from './lib';
export * from './service/router';
export * from './types';
+export * from './database/badgesStore';
diff --git a/plugins/badges-backend/src/service/router-obfuscated.test.ts b/plugins/badges-backend/src/service/router-obfuscated.test.ts
new file mode 100644
index 0000000000..3c9ec0d2b6
--- /dev/null
+++ b/plugins/badges-backend/src/service/router-obfuscated.test.ts
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import express from 'express';
+import request from 'supertest';
+import {
+ getVoidLogger,
+ PluginEndpointDiscovery,
+ ServerTokenManager,
+ SingleHostDiscovery,
+} from '@backstage/backend-common';
+import { CatalogApi } from '@backstage/catalog-client';
+import type { Entity } from '@backstage/catalog-model';
+import { Config, ConfigReader } from '@backstage/config';
+import { createRouter } from './router';
+import { BadgeBuilder } from '../lib';
+import {
+ BackstageIdentityResponse,
+ IdentityApiGetIdentityRequest,
+} from '@backstage/plugin-auth-node';
+import { BadgesStore } from '../database/badgesStore';
+
+describe('createRouter', () => {
+ let app: express.Express;
+ const badgeBuilder: jest.Mocked = {
+ getBadges: jest.fn(),
+ createBadgeJson: jest.fn(),
+ createBadgeSvg: jest.fn(),
+ };
+
+ const catalog = {
+ addLocation: jest.fn(),
+ getEntities: jest.fn(),
+ getEntityByRef: jest.fn(),
+ getLocationByRef: jest.fn(),
+ getLocationById: jest.fn(),
+ removeLocationById: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ refreshEntity: jest.fn(),
+ getEntityAncestors: jest.fn(),
+ getEntityFacets: jest.fn(),
+ validateEntity: jest.fn(),
+ };
+ const getIdentity = jest
+ .fn()
+ .mockImplementation(
+ async ({
+ request: _request,
+ }: IdentityApiGetIdentityRequest): Promise<
+ BackstageIdentityResponse | undefined
+ > => {
+ return {
+ identity: {
+ userEntityRef: 'user:default/guest',
+ ownershipEntityRefs: [],
+ type: 'user',
+ },
+ token: 'token',
+ };
+ },
+ );
+
+ const config: Config = new ConfigReader({
+ backend: {
+ baseUrl: 'http://127.0.0.1',
+ listen: {
+ port: 7007,
+ },
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ },
+ },
+ app: {
+ badges: {
+ obfuscate: true,
+ },
+ },
+ });
+
+ let discovery: PluginEndpointDiscovery;
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'test',
+ },
+ };
+
+ const entities: Entity[] = [
+ entity,
+ {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'test-2',
+ },
+ },
+ ];
+
+ const badge = {
+ id: 'test-badge',
+ badge: {
+ label: 'test',
+ message: 'badge',
+ },
+ url: '/...',
+ markdown: '[]',
+ };
+
+ const badgeEntity = {
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ };
+
+ const badgeStore: jest.Mocked = {
+ getBadgeUuid: jest.fn().mockImplementation(async () => {
+ return { uuid: 'uuid1' };
+ }),
+ getBadgeFromUuid: jest.fn().mockImplementation(async () => {
+ return badgeEntity;
+ }),
+ };
+
+ beforeAll(async () => {
+ discovery = SingleHostDiscovery.fromConfig(config);
+ const tokenManager = ServerTokenManager.noop();
+ const router = await createRouter({
+ badgeBuilder,
+ catalog: catalog as Partial as CatalogApi,
+ config,
+ discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('works with provided badgeStore', async () => {
+ const tokenManager = ServerTokenManager.noop();
+ const router = await createRouter({
+ badgeBuilder,
+ catalog: catalog as Partial as CatalogApi,
+ config,
+ discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
+ badgeStore: badgeStore,
+ });
+ expect(router).toBeDefined();
+ });
+
+ describe('GET /entity/:namespace/:kind/:name/badge-specs', () => {
+ it('does not returns all badge specs for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+
+ badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ const response = await request(app).get(
+ '/entity/default/component/test/badge-specs',
+ );
+
+ expect(response.status).toEqual(404);
+ });
+ });
+
+ describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
+ it('does not returns badge for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+
+ const image = '';
+ badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
+
+ const response = await request(app).get(
+ '/entity/default/component/test/badge/test-badge',
+ );
+
+ expect(response.status).toEqual(404);
+ });
+
+ it('does not returns badge spec for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ const url = '/entity/default/component/test/badge/test-badge?format=json';
+ const response = await request(app).get(url);
+
+ expect(response.status).toEqual(404);
+ });
+ });
+
+ describe('GET /entity/:namespace/:kind/:name/obfuscated', () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+
+ it('returns obfuscated 401 if no auth', async () => {
+ const obfuscatedEntity = await request(app).get(
+ '/entity/default/component/test/obfuscated',
+ );
+ expect(obfuscatedEntity.status).toEqual(401);
+ });
+
+ it('returns obfuscated entity and badges', async () => {
+ const obfuscatedEntity = await request(app)
+ .get('/entity/default/component/test/obfuscated')
+ .set('Authorization', 'Bearer fakeToken');
+ expect(obfuscatedEntity.status).toEqual(200);
+ expect(obfuscatedEntity.body.uuid).toMatch(
+ new RegExp(
+ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
+ ),
+ );
+
+ const uuid = obfuscatedEntity.body.uuid;
+ const url = `/entity/${uuid}/test-badge?format=json`;
+ let response = await request(app).get(url);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(badge);
+
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+
+ const image = '';
+ badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
+
+ response = await request(app).get(`/entity/${uuid}/test-badge`);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(Buffer.from(image));
+
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+
+ badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ response = await request(app).get(`/entity/${uuid}/badge-specs`);
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual([badge]);
+
+ expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1);
+ expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(2);
+ });
+
+ describe('Errors', () => {
+ it('returns 404 for unknown entity uuid', async () => {
+ badgeStore.getBadgeFromUuid.mockResolvedValue(undefined);
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+ badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ async function testUrl(url: string) {
+ const response = await request(app).get(url);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({
+ error: {
+ message: expect.any(String),
+ name: 'NotFoundError',
+ },
+ request: {
+ method: 'GET',
+ url,
+ },
+ response: {
+ statusCode: 404,
+ },
+ });
+ }
+ await testUrl(
+ '/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs',
+ );
+ await testUrl(
+ '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge',
+ );
+ });
+ });
+ });
+});
diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts
index 109b64231d..2ecae6b452 100644
--- a/plugins/badges-backend/src/service/router.test.ts
+++ b/plugins/badges-backend/src/service/router.test.ts
@@ -17,7 +17,9 @@
import express from 'express';
import request from 'supertest';
import {
+ getVoidLogger,
PluginEndpointDiscovery,
+ ServerTokenManager,
SingleHostDiscovery,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
@@ -25,10 +27,16 @@ import type { Entity } from '@backstage/catalog-model';
import { Config, ConfigReader } from '@backstage/config';
import { createRouter } from './router';
import { BadgeBuilder } from '../lib';
+import {
+ BackstageIdentityResponse,
+ IdentityApiGetIdentityRequest,
+} from '@backstage/plugin-auth-node';
+import { BadgesStore } from '../database/badgesStore';
describe('createRouter', () => {
let app: express.Express;
let badgeBuilder: jest.Mocked;
+
const catalog = {
addLocation: jest.fn(),
getEntities: jest.fn(),
@@ -42,12 +50,15 @@ describe('createRouter', () => {
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
+
+ const getIdentity = jest.fn();
+
let config: Config;
let discovery: PluginEndpointDiscovery;
const entity: Entity = {
apiVersion: 'v1',
- kind: 'service',
+ kind: 'component',
metadata: {
name: 'test',
},
@@ -63,6 +74,11 @@ describe('createRouter', () => {
markdown: '[]',
};
+ const badgeStore: jest.Mocked = {
+ getBadgeFromUuid: jest.fn(),
+ getBadgeUuid: jest.fn(),
+ };
+
beforeAll(async () => {
badgeBuilder = {
getBadges: jest.fn(),
@@ -73,15 +89,40 @@ describe('createRouter', () => {
backend: {
baseUrl: 'http://127.0.0.1',
listen: { port: 7007 },
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ },
},
});
- discovery = SingleHostDiscovery.fromConfig(config);
+ getIdentity.mockImplementation(
+ async ({
+ request: _request,
+ }: IdentityApiGetIdentityRequest): Promise<
+ BackstageIdentityResponse | undefined
+ > => {
+ return {
+ identity: {
+ userEntityRef: 'user:default/guest',
+ ownershipEntityRefs: [],
+ type: 'user',
+ },
+ token: 'token',
+ };
+ },
+ );
+
+ discovery = SingleHostDiscovery.fromConfig(config);
+ const tokenManager = ServerTokenManager.noop();
const router = await createRouter({
badgeBuilder,
catalog: catalog as Partial as CatalogApi,
config,
discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
});
app = express().use(router);
});
@@ -90,12 +131,17 @@ describe('createRouter', () => {
jest.resetAllMocks();
});
- it('works', async () => {
+ it('works with badgeStore', async () => {
+ const tokenManager = ServerTokenManager.noop();
const router = await createRouter({
badgeBuilder,
catalog: catalog as Partial as CatalogApi,
config,
discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
+ badgeStore: badgeStore,
});
expect(router).toBeDefined();
});
@@ -108,7 +154,7 @@ describe('createRouter', () => {
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
const response = await request(app).get(
- '/entity/default/service/test/badge-specs',
+ '/entity/default/component/test/badge-specs',
);
expect(response.status).toEqual(200);
@@ -118,10 +164,10 @@ describe('createRouter', () => {
expect(catalog.getEntityByRef).toHaveBeenCalledWith(
{
namespace: 'default',
- kind: 'service',
+ kind: 'component',
name: 'test',
},
- { token: undefined },
+ { token: '' },
);
expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1);
@@ -130,46 +176,7 @@ describe('createRouter', () => {
badgeInfo: { id: badge.id },
context: {
badgeUrl: expect.stringMatching(
- /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/,
- ),
- config,
- entity,
- },
- });
- });
- });
-
- describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
- it('returns badge for entity', async () => {
- catalog.getEntityByRef.mockResolvedValueOnce(entity);
-
- const image = '';
- badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
-
- const response = await request(app).get(
- '/entity/default/service/test/badge/test-badge',
- );
-
- expect(response.status).toEqual(200);
- expect(response.body).toEqual(Buffer.from(image));
-
- expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
- expect(catalog.getEntityByRef).toHaveBeenCalledWith(
- {
- namespace: 'default',
- kind: 'service',
- name: 'test',
- },
- { token: undefined },
- );
-
- expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0);
- expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1);
- expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({
- badgeInfo: { id: badge.id },
- context: {
- badgeUrl: expect.stringMatching(
- /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/,
+ /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/component\/test\/badge\/test-badge/,
),
config,
entity,
@@ -177,27 +184,90 @@ describe('createRouter', () => {
});
});
- it('returns badge spec for entity', async () => {
- catalog.getEntityByRef.mockResolvedValueOnce(entity);
- badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+ describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
+ it('returns badge for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
- const url = '/entity/default/service/test/badge/test-badge?format=json';
- const response = await request(app).get(url);
+ const image = '';
+ badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
- expect(response.status).toEqual(200);
- expect(response.body).toEqual(badge);
+ const response = await request(app).get(
+ '/entity/default/component/test/badge/test-badge',
+ );
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(Buffer.from(image));
+
+ expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
+ expect(catalog.getEntityByRef).toHaveBeenCalledWith(
+ {
+ namespace: 'default',
+ kind: 'component',
+ name: 'test',
+ },
+ { token: '' },
+ );
+
+ expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0);
+ expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1);
+ expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({
+ badgeInfo: { id: badge.id },
+ context: {
+ badgeUrl: expect.stringMatching(
+ /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/component\/test\/badge\/test-badge/,
+ ),
+ config,
+ entity,
+ },
+ });
+ });
+
+ it('returns badge spec for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ const url =
+ '/entity/default/component/test/badge/test-badge?format=json';
+ const response = await request(app).get(url);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(badge);
+ });
});
- });
- describe('Errors', () => {
- it('returns 404 for unknown entities', async () => {
+ describe('Errors', () => {
+ it('returns 404 for unknown entities', async () => {
+ catalog.getEntityByRef.mockResolvedValue(undefined);
+ async function testUrl(url: string) {
+ const response = await request(app).get(url);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({
+ error: {
+ message: 'No component entity in default named "missing"',
+ name: 'NotFoundError',
+ },
+ request: {
+ method: 'GET',
+ url,
+ },
+ response: {
+ statusCode: 404,
+ },
+ });
+ }
+ await testUrl('/entity/default/component/missing/badge-specs');
+ await testUrl('/entity/default/component/missing/badge/test-badge');
+ });
+ });
+
+ it('returns 404 for uuid entities', async () => {
catalog.getEntityByRef.mockResolvedValue(undefined);
async function testUrl(url: string) {
const response = await request(app).get(url);
expect(response.status).toEqual(404);
expect(response.body).toEqual({
error: {
- message: 'No service entity in default named "missing"',
+ message: 'No component entity in default named "missing"',
name: 'NotFoundError',
},
request: {
@@ -209,8 +279,8 @@ describe('createRouter', () => {
},
});
}
- await testUrl('/entity/default/service/missing/badge-specs');
- await testUrl('/entity/default/service/missing/badge/test-badge');
+ await testUrl('/entity/default/component/missing/badge-specs');
+ await testUrl('/entity/default/component/missing/badge/test-badge');
});
});
});
diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts
index e5caa5cd2a..308865b73a 100644
--- a/plugins/badges-backend/src/service/router.ts
+++ b/plugins/badges-backend/src/service/router.ts
@@ -17,14 +17,21 @@
import express from 'express';
import Router from 'express-promise-router';
import {
+ DatabaseManager,
errorHandler,
PluginEndpointDiscovery,
+ TokenManager,
} from '@backstage/backend-common';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
-import { NotFoundError } from '@backstage/errors';
+import { AuthenticationError, NotFoundError } from '@backstage/errors';
import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder';
import { BadgeContext, BadgeFactories } from '../types';
+import { isNil } from 'lodash';
+import { Logger } from 'winston';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
+import { BadgesStore, DatabaseBadgesStore } from '../database/badgesStore';
/** @public */
export interface RouterOptions {
@@ -33,6 +40,10 @@ export interface RouterOptions {
catalog?: CatalogApi;
config: Config;
discovery: PluginEndpointDiscovery;
+ tokenManager: TokenManager;
+ logger: Logger;
+ identity: IdentityApi;
+ badgeStore?: BadgesStore;
}
/** @public */
@@ -46,13 +57,214 @@ export async function createRouter(
new DefaultBadgeBuilder(options.badgeFactories || {});
const router = Router();
+ const { config, logger, tokenManager, discovery, identity } = options;
+ const baseUrl = await discovery.getExternalBaseUrl('badges');
+
+ if (config.getOptionalBoolean('app.badges.obfuscate')) {
+ return obfuscatedRoute(
+ router,
+ catalog,
+ badgeBuilder,
+ tokenManager,
+ logger,
+ options,
+ config,
+ identity,
+ baseUrl,
+ );
+ }
+ return nonObfuscatedRoute(
+ router,
+ catalog,
+ badgeBuilder,
+ tokenManager,
+ config,
+ baseUrl,
+ );
+}
+
+async function obfuscatedRoute(
+ router: express.Router,
+ catalog: CatalogApi,
+ badgeBuilder: BadgeBuilder,
+ tokenManager: TokenManager,
+ logger: Logger,
+ options: RouterOptions,
+ config: Config,
+ identity: IdentityApi,
+ baseUrl: string,
+) {
+ logger.info('Badges obfuscation is enabled');
+
+ const store = options.badgeStore
+ ? options.badgeStore
+ : await DatabaseBadgesStore.create({
+ database: await DatabaseManager.fromConfig(config).forPlugin('badges'),
+ });
+
+ router.get('/entity/:entityUuid/badge-specs', async (req, res) => {
+ const { entityUuid } = req.params;
+
+ // Retrieve the badge info from the database
+ const badgeInfos = await store.getBadgeFromUuid(entityUuid);
+
+ if (isNil(badgeInfos)) {
+ throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`);
+ }
+
+ // If a mapping is found, map name, namespace and kind
+ const name = badgeInfos.name;
+ const namespace = badgeInfos.namespace;
+ const kind = badgeInfos.kind;
+ const token = await tokenManager.getToken();
+
+ // Query the catalog with the name, namespace, kind to get the entity informations
+ const entity = await catalog.getEntityByRef(
+ {
+ namespace,
+ kind,
+ name,
+ },
+ token,
+ );
+ if (isNil(entity)) {
+ throw new NotFoundError(
+ `No ${kind} entity in ${namespace} named "${name}"`,
+ );
+ }
+
+ // Create the badge specs
+ const specs = [];
+ for (const badgeInfo of await badgeBuilder.getBadges()) {
+ const context: BadgeContext = {
+ badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeInfo.id}`,
+ config: config,
+ entity,
+ };
+
+ const badge = await badgeBuilder.createBadgeJson({
+ badgeInfo,
+ context,
+ });
+ specs.push(badge);
+ }
+
+ res.status(200).json(specs);
+ });
+
+ router.get('/entity/:entityUuid/:badgeId', async (req, res) => {
+ const { entityUuid, badgeId } = req.params;
+
+ // Retrieve the badge info from the database
+ const badgeInfo = await store.getBadgeFromUuid(entityUuid);
+
+ if (isNil(badgeInfo)) {
+ throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`);
+ }
+
+ // If a mapping is found, map name, namespace and kind
+ const name = badgeInfo.name;
+ const namespace = badgeInfo.namespace;
+ const kind = badgeInfo.kind;
+ const token = await tokenManager.getToken();
+ const entity = await catalog.getEntityByRef(
+ {
+ namespace,
+ kind,
+ name,
+ },
+ token,
+ );
+ if (isNil(entity)) {
+ throw new NotFoundError(
+ `No ${kind} entity in ${namespace} named "${name}"`,
+ res.sendStatus(404),
+ );
+ }
+
+ let format =
+ req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml';
+ if (req.query.format === 'json') {
+ format = 'application/json';
+ }
+
+ const badgeOptions = {
+ badgeInfo: { id: badgeId },
+ context: {
+ badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeId}`,
+ config: config,
+ entity,
+ },
+ };
+
+ let data: string;
+ if (format === 'application/json') {
+ data = JSON.stringify(
+ await badgeBuilder.createBadgeJson(badgeOptions),
+ null,
+ 2,
+ );
+ } else {
+ data = await badgeBuilder.createBadgeSvg(badgeOptions);
+ }
+
+ res.setHeader('Content-Type', format);
+ res.status(200).send(data);
+ });
+
+ router.get(
+ '/entity/:namespace/:kind/:name/obfuscated',
+ function authenticate(req, _res, next) {
+ const token =
+ getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
+ (req.cookies?.token as string | undefined);
+
+ if (!token) {
+ throw new AuthenticationError('Unauthorized');
+ }
+
+ try {
+ req.user = identity.getIdentity({ request: req });
+ next();
+ } catch (error) {
+ tokenManager.authenticate(token.toString());
+ next(error);
+ }
+ },
+ async (req, res) => {
+ const { namespace, kind, name } = req.params;
+ const storedEntityUuid: { uuid: string } | undefined =
+ await store.getBadgeUuid(name, namespace, kind);
+
+ if (isNil(storedEntityUuid)) {
+ throw new NotFoundError(
+ `No uuid found for entity "${namespace}/${kind}/${name}"`,
+ );
+ }
+
+ return res.status(200).json(storedEntityUuid);
+ },
+ );
+
+ router.use(errorHandler());
+
+ return router;
+}
+
+async function nonObfuscatedRoute(
+ router: express.Router,
+ catalog: CatalogApi,
+ badgeBuilder: BadgeBuilder,
+ tokenManager: TokenManager,
+ config: Config,
+ baseUrl: string,
+) {
router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => {
+ const token = await tokenManager.getToken();
const { namespace, kind, name } = req.params;
const entity = await catalog.getEntityByRef(
{ namespace, kind, name },
- {
- token: getBearerToken(req.headers.authorization),
- },
+ token,
);
if (!entity) {
throw new NotFoundError(
@@ -62,19 +274,17 @@ export async function createRouter(
const specs = [];
for (const badgeInfo of await badgeBuilder.getBadges()) {
+ const badgeId = badgeInfo.id;
const context: BadgeContext = {
- badgeUrl: await getBadgeUrl(
- namespace,
- kind,
- name,
- badgeInfo.id,
- options,
- ),
- config: options.config,
+ badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`,
+ config: config,
entity,
};
- const badge = await badgeBuilder.createBadgeJson({ badgeInfo, context });
+ const badge = await badgeBuilder.createBadgeJson({
+ badgeInfo,
+ context,
+ });
specs.push(badge);
}
@@ -85,11 +295,10 @@ export async function createRouter(
'/entity/:namespace/:kind/:name/badge/:badgeId',
async (req, res) => {
const { namespace, kind, name, badgeId } = req.params;
+ const token = await tokenManager.getToken();
const entity = await catalog.getEntityByRef(
{ namespace, kind, name },
- {
- token: getBearerToken(req.headers.authorization),
- },
+ token,
);
if (!entity) {
throw new NotFoundError(
@@ -106,8 +315,8 @@ export async function createRouter(
const badgeOptions = {
badgeInfo: { id: badgeId },
context: {
- badgeUrl: await getBadgeUrl(namespace, kind, name, badgeId, options),
- config: options.config,
+ badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`,
+ config: config,
entity,
},
};
@@ -132,18 +341,3 @@ export async function createRouter(
return router;
}
-
-async function getBadgeUrl(
- namespace: string,
- kind: string,
- name: string,
- badgeId: string,
- options: RouterOptions,
-): Promise {
- const baseUrl = await options.discovery.getExternalBaseUrl('badges');
- return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`;
-}
-
-function getBearerToken(header?: string): string | undefined {
- return header?.match(/Bearer\s+(\S+)/i)?.[1];
-}
diff --git a/plugins/badges-backend/src/service/standaloneServer.ts b/plugins/badges-backend/src/service/standaloneServer.ts
index 0800585634..b07e1fa3b1 100644
--- a/plugins/badges-backend/src/service/standaloneServer.ts
+++ b/plugins/badges-backend/src/service/standaloneServer.ts
@@ -19,9 +19,14 @@ import { Logger } from 'winston';
import {
createServiceBuilder,
loadBackendConfig,
+ ServerTokenManager,
SingleHostDiscovery,
+ useHotMemoize,
} from '@backstage/backend-common';
import { createRouter } from './router';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+import { DatabaseBadgesStore } from '../database/badgesStore';
+import Knex from 'knex';
export interface ServerOptions {
port: number;
@@ -35,10 +40,40 @@ export async function startStandaloneServer(
const logger = options.logger.child({ service: 'badges-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
+ const database = useHotMemoize(module, () => {
+ return Knex({
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ });
+ });
logger.debug('Creating application...');
- const router = await createRouter({ config, discovery });
+ const tokenManager = ServerTokenManager.noop();
+ const identity: IdentityApi = {
+ async getIdentity({ request }) {
+ const token = request.headers.authorization?.split(' ')[1];
+ return {
+ identity: {
+ type: 'user',
+ ownershipEntityRefs: [],
+ userEntityRef: token || 'user:default/john_doe',
+ },
+ token: token || 'no-token',
+ };
+ },
+ };
+ const router = await createRouter({
+ config,
+ discovery,
+ tokenManager,
+ logger,
+ identity,
+ badgeStore: await DatabaseBadgesStore.create({
+ database: { getClient: async () => database },
+ }),
+ });
let service = createServiceBuilder(module)
.setPort(options.port)
diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md
index b4a0e6aec6..a05ef8405d 100644
--- a/plugins/badges/CHANGELOG.md
+++ b/plugins/badges/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/plugin-badges
+## 0.2.42-next.2
+
+### Patch Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.42-next.1
### Patch Changes
diff --git a/plugins/badges/README.md b/plugins/badges/README.md
index 65ace6226e..4b8ff38d47 100644
--- a/plugins/badges/README.md
+++ b/plugins/badges/README.md
@@ -18,6 +18,51 @@ This will popup a badges dialog showing all available badges for that entity lik

+## Badge obfuscation
+
+The badges plugin supports obfuscating the badge URL to prevent it from being enumerated if the badges are used in a public context (like in Github repositories).
+
+To enable obfuscation, set the `obfuscate` option to `true` in the `app.badges` section of your `app-config.yaml`:
+
+```yaml
+app:
+ badges:
+ obfuscate: true
+```
+
+Please note that if you have already set badges in your repositories and you activate the obfuscation you will need to update the badges in your repositories to use the new obfuscated URLs.
+
+Please note that the backend part needs to be configured to support obfuscation. See the [backend plugin documentation](../badges-backend/README.md) for more details.
+
+Also, you need to allow your frontend to access the configuration see :
+
+Example implementation would be in : `packages/app/src/config.d.ts`
+
+```typescript
+export interface Config {
+ app: {
+ ... some code
+ badges: {
+ /**
+ * badges obfuscate
+ * @visibility frontend
+ */
+ obfuscate?: string;
+ };
+ };
+}
+```
+
+then include in the `packages/app/package.json` :
+
+```json
+"files": [
+ "dist",
+ "config.d.ts"
+ ],
+"configSchema": "config.d.ts",
+```
+
## Sample Badges
Here are some samples of badges for the `artists-lookup` service in the Demo Backstage site:
diff --git a/plugins/badges/package.json b/plugins/badges/package.json
index 0e736d1cbc..be2f7adc79 100644
--- a/plugins/badges/package.json
+++ b/plugins/badges/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-badges",
"description": "A Backstage plugin that generates README badges for your entities",
- "version": "0.2.42-next.1",
+ "version": "0.2.42-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -50,14 +50,9 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
- "@testing-library/dom": "^8.0.0",
- "@testing-library/jest-dom": "^5.10.1",
- "@testing-library/react": "^12.1.3",
- "@testing-library/user-event": "^14.0.0",
+ "@testing-library/jest-dom": "^5.16.5",
"@types/node": "^16.11.26",
- "@types/react": "^16.13.1 || ^17.0.0",
- "cross-fetch": "^3.1.5",
- "msw": "^1.0.0"
+ "cross-fetch": "^3.1.5"
},
"files": [
"dist"
diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts
index 9a89806d56..9a08db882a 100644
--- a/plugins/badges/src/api/BadgesClient.ts
+++ b/plugins/badges/src/api/BadgesClient.ts
@@ -18,31 +18,55 @@ import { generatePath } from 'react-router-dom';
import { ResponseError } from '@backstage/errors';
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { BadgesApi, BadgeSpec } from './types';
-import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
+import { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
export class BadgesClient implements BadgesApi {
private readonly discoveryApi: DiscoveryApi;
- private readonly identityApi: IdentityApi;
+ private readonly fetchApi: FetchApi;
+ private readonly configApi: ConfigApi;
constructor(options: {
discoveryApi: DiscoveryApi;
- identityApi: IdentityApi;
+ fetchApi: FetchApi;
+ configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
- this.identityApi = options.identityApi;
+ this.fetchApi = options.fetchApi;
+ this.configApi = options.configApi;
+ }
+
+ static fromConfig(options: {
+ fetchApi: FetchApi;
+ discoveryApi: DiscoveryApi;
+ configApi: ConfigApi;
+ }) {
+ return new BadgesClient(options);
}
public async getEntityBadgeSpecs(entity: Entity): Promise {
- const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
- const { token } = await this.identityApi.getCredentials();
- const response = await fetch(entityBadgeSpecsUrl, {
- headers: token
- ? {
- Authorization: `Bearer ${token}`,
- }
- : undefined,
- });
+ // Check if obfuscation is enabled in the configuration
+ const obfuscate = this.configApi.getOptionalBoolean('app.badges.obfuscate');
+ if (obfuscate) {
+ const entityUuidUrl = await this.getEntityUuidUrl(entity);
+ const entityUuid = await this.getEntityUuid(entityUuidUrl).then(data => {
+ return data.uuid;
+ });
+ const entityUuidBadgeSpecsUrl = await this.getEntityUuidBadgeSpecsUrl(
+ entityUuid,
+ );
+
+ const response = await this.fetchApi.fetch(entityUuidBadgeSpecsUrl);
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ }
+
+ return await response.json();
+ }
+
+ // If obfuscation is disabled, get the badge specs directly as the previous implementation
+ const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
+ const response = await this.fetchApi.fetch(entityBadgeSpecsUrl);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
@@ -50,14 +74,39 @@ export class BadgesClient implements BadgesApi {
return await response.json();
}
+ private async getEntityUuidUrl(entity: Entity): Promise {
+ const routeParams = this.getEntityRouteParams(entity);
+ const path = generatePath(`:namespace/:kind/:name`, routeParams);
+ const baseUrl = await this.discoveryApi.getBaseUrl('badges');
+ const obfuscatedEntityUrl = `${baseUrl}/entity/${path}/obfuscated`;
+
+ return obfuscatedEntityUrl;
+ }
+
+ private async getEntityUuid(entityUuidUrl: string): Promise {
+ const responseEntityUuid = await this.fetchApi.fetch(entityUuidUrl);
+
+ if (!responseEntityUuid.ok) {
+ throw await ResponseError.fromResponse(responseEntityUuid);
+ }
+ return await responseEntityUuid.json();
+ }
+
+ private async getEntityUuidBadgeSpecsUrl(entityUuid: {
+ uuid: string;
+ }): Promise {
+ const baseUrl = await this.discoveryApi.getBaseUrl('badges');
+ return `${baseUrl}/entity/${entityUuid}/badge-specs`;
+ }
+
private async getEntityBadgeSpecsUrl(entity: Entity): Promise {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(`:namespace/:kind/:name`, routeParams);
- return `${await this.discoveryApi.getBaseUrl(
- 'badges',
- )}/entity/${path}/badge-specs`;
+ const baseUrl = await this.discoveryApi.getBaseUrl('badges');
+ return `${baseUrl}/entity/${path}/badge-specs`;
}
+ // This function is used to generate the route parameters using the entity kind, namespace and name
private getEntityRouteParams(entity: Entity) {
return {
kind: entity.kind.toLocaleLowerCase('en-US'),
diff --git a/plugins/badges/src/plugin.ts b/plugins/badges/src/plugin.ts
index 7ef3025458..771e3ed893 100644
--- a/plugins/badges/src/plugin.ts
+++ b/plugins/badges/src/plugin.ts
@@ -15,11 +15,12 @@
*/
import { badgesApiRef, BadgesClient } from './api';
import {
+ configApiRef,
createApiFactory,
createComponentExtension,
createPlugin,
+ fetchApiRef,
discoveryApiRef,
- identityApiRef,
} from '@backstage/core-plugin-api';
/** @public */
@@ -28,9 +29,17 @@ export const badgesPlugin = createPlugin({
apis: [
createApiFactory({
api: badgesApiRef,
- deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
- factory: ({ discoveryApi, identityApi }) =>
- new BadgesClient({ discoveryApi, identityApi }),
+ deps: {
+ fetchApi: fetchApiRef,
+ discoveryApi: discoveryApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ fetchApi, discoveryApi, configApi }) =>
+ new BadgesClient({
+ fetchApi,
+ discoveryApi,
+ configApi,
+ }),
}),
],
});
diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md
index 298c684d3e..8f7de5e0f2 100644
--- a/plugins/bazaar/CHANGELOG.md
+++ b/plugins/bazaar/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-bazaar
+## 0.2.8-next.2
+
+### Patch Changes
+
+- 900880ab7c3: Fixed `validateDOMNesting` warnings
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.8-next.1
### Patch Changes
diff --git a/plugins/bazaar/dev/__fixtures__/get-projects-response.json b/plugins/bazaar/dev/__fixtures__/get-projects-response.json
new file mode 100644
index 0000000000..7aee019a6e
--- /dev/null
+++ b/plugins/bazaar/dev/__fixtures__/get-projects-response.json
@@ -0,0 +1,35 @@
+{
+ "status": "ok",
+ "data": [
+ {
+ "id": 10,
+ "entity_ref": null,
+ "title": "Lorem ipsum 1",
+ "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+ "status": "proposed",
+ "updated_at": "2023-03-10T12:41:41.941Z",
+ "community": "",
+ "size": "medium",
+ "start_date": null,
+ "end_date": null,
+ "responsible": "Responsible Adult 1",
+ "docs": null,
+ "members_count": "3"
+ },
+ {
+ "id": 11,
+ "entity_ref": null,
+ "title": "Lorem ipsum 2",
+ "description": "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud",
+ "status": "proposed",
+ "updated_at": "2023-03-09T12:41:41.941Z",
+ "community": "",
+ "size": "medium",
+ "start_date": null,
+ "end_date": null,
+ "responsible": "Responsible Adult 2",
+ "docs": null,
+ "members_count": "5"
+ }
+ ]
+}
diff --git a/plugins/bazaar/dev/index.tsx b/plugins/bazaar/dev/index.tsx
index a4509bc1fc..f2c40df8e1 100644
--- a/plugins/bazaar/dev/index.tsx
+++ b/plugins/bazaar/dev/index.tsx
@@ -16,10 +16,30 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { bazaarPlugin, BazaarPage } from '../src/plugin';
+import { bazaarApiRef } from '../src/api';
+
+import getProjectsData from './__fixtures__/get-projects-response.json';
createDevApp()
.registerPlugin(bazaarPlugin)
+ .registerApi({
+ api: bazaarApiRef,
+ deps: {},
+ factory: () =>
+ ({
+ getProjects: async () => getProjectsData,
+ } as any),
+ })
+ .registerApi({
+ api: catalogApiRef,
+ deps: {},
+ factory: () =>
+ ({
+ getEntities: () => ({}),
+ } as CatalogApi),
+ })
.addPage({
element: ,
title: 'Root Page',
diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json
index c61db47231..5e8bb28df6 100644
--- a/plugins/bazaar/package.json
+++ b/plugins/bazaar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bazaar",
- "version": "0.2.8-next.1",
+ "version": "0.2.8-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx
index 97b247e289..61d1dfebb9 100644
--- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx
+++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx
@@ -94,11 +94,7 @@ export const ProjectCard = ({
setOpenCard(true)}>
- {title}
-
- }
+ title={title}
subtitle={`updated ${DateTime.fromISO(
new Date(updatedAt!).toISOString(),
).toRelative({
diff --git a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx
index 8f7160600a..39319b6353 100644
--- a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx
+++ b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx
@@ -15,9 +15,14 @@
*/
import React, { ChangeEvent, useState } from 'react';
-import { Content } from '@backstage/core-components';
import { ProjectCard } from '../ProjectCard/ProjectCard';
-import { makeStyles, Grid, TablePagination, GridSize } from '@material-ui/core';
+import {
+ Box,
+ makeStyles,
+ Grid,
+ TablePagination,
+ GridSize,
+} from '@material-ui/core';
import { BazaarProject } from '../../types';
import { Entity } from '@backstage/catalog-model';
@@ -80,7 +85,7 @@ export const ProjectPreview = ({
}
return (
-
+
{bazaarProjects
.slice((page - 1) * rows, rows * page)
@@ -101,6 +106,7 @@ export const ProjectPreview = ({
{useTablePagination && (
)}
-
+
);
};
diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md
index 1ef42a6bbe..2fba6da9b3 100644
--- a/plugins/bitrise/CHANGELOG.md
+++ b/plugins/bitrise/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-bitrise
+## 0.1.45-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.45-next.1
### Patch Changes
diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json
index 1b3740a058..1c1c3523ea 100644
--- a/plugins/bitrise/package.json
+++ b/plugins/bitrise/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-bitrise",
"description": "A Backstage plugin that integrates towards Bitrise",
- "version": "0.1.45-next.1",
+ "version": "0.1.45-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md
index f7ffc5e63c..886703f0c8 100644
--- a/plugins/catalog-backend-module-aws/CHANGELOG.md
+++ b/plugins/catalog-backend-module-aws/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-backend-module-aws
+## 0.1.19-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-kubernetes-common@0.6.3-next.0
+ - @backstage/config@1.0.7
+
## 0.1.19-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md
index 491dbf497e..83b072061b 100644
--- a/plugins/catalog-backend-module-aws/api-report.md
+++ b/plugins/catalog-backend-module-aws/api-report.md
@@ -3,11 +3,12 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import { AwsCredentialIdentity } from '@aws-sdk/types';
+import { AwsCredentialsManager } from '@backstage/integration-aws-node';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
-import { Credentials } from 'aws-sdk';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
@@ -19,11 +20,16 @@ import { UrlReader } from '@backstage/backend-common';
// @public
export type AWSCredentialFactory = (
awsAccountId: string,
-) => Promise;
+) => Promise;
// @public
export class AwsEKSClusterProcessor implements CatalogProcessor {
- constructor(options: { credentialsFactory?: AWSCredentialFactory });
+ constructor(options: {
+ credentialsFactory?: AWSCredentialFactory;
+ credentialsManager?: AwsCredentialsManager;
+ });
+ // (undocumented)
+ static fromConfig(configRoot: Config): AwsEKSClusterProcessor;
// (undocumented)
getProcessorName(): string;
// (undocumented)
@@ -44,7 +50,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
options: {
logger: Logger;
},
- ): AwsOrganizationCloudAccountProcessor;
+ ): Promise;
// (undocumented)
getProcessorName(): string;
// (undocumented)
diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json
index 73bd5123d2..50d7349cef 100644
--- a/plugins/catalog-backend-module-aws/package.json
+++ b/plugins/catalog-backend-module-aws/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-aws",
"description": "A Backstage catalog backend module that helps integrate towards AWS",
- "version": "0.1.19-next.1",
+ "version": "0.1.19-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -45,7 +45,12 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
- "@aws-sdk/client-s3": "^3.208.0",
+ "@aws-sdk/client-eks": "^3.310.0",
+ "@aws-sdk/client-organizations": "^3.310.0",
+ "@aws-sdk/client-s3": "^3.310.0",
+ "@aws-sdk/credential-providers": "^3.310.0",
+ "@aws-sdk/middleware-endpoint": "^3.310.0",
+ "@aws-sdk/types": "^3.310.0",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
@@ -53,23 +58,23 @@
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
+ "@backstage/integration-aws-node": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-kubernetes-common": "workspace:^",
"@backstage/types": "workspace:^",
- "aws-sdk": "^2.840.0",
"lodash": "^4.17.21",
"p-limit": "^3.0.2",
"uuid": "^8.0.0",
"winston": "^3.2.1"
},
"devDependencies": {
- "@aws-sdk/util-stream-node": "^3.208.0",
+ "@aws-sdk/util-stream-node": "^3.310.0",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
"aws-sdk-client-mock": "^2.0.0",
- "aws-sdk-mock": "^5.2.1",
+ "aws-sdk-client-mock-jest": "^2.0.0",
"luxon": "^3.0.0",
"yaml": "^2.0.0"
},
diff --git a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts
deleted file mode 100644
index fded78aed4..0000000000
--- a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2022 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import aws, { Credentials } from 'aws-sdk';
-import { CredentialsOptions } from 'aws-sdk/lib/credentials';
-
-export class AwsCredentials {
- /**
- * If accessKeyId and secretAccessKey are missing, the DefaultAWSCredentialsProviderChain will be used:
- * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
- */
- static create(
- config: {
- accessKeyId?: string;
- secretAccessKey?: string;
- roleArn?: string;
- externalId?: string;
- },
- roleSessionName: string,
- ): Credentials | CredentialsOptions | undefined {
- if (!config) {
- return undefined;
- }
-
- const accessKeyId = config.accessKeyId;
- const secretAccessKey = config.secretAccessKey;
- let explicitCredentials: Credentials | undefined;
-
- if (accessKeyId && secretAccessKey) {
- explicitCredentials = new Credentials({
- accessKeyId,
- secretAccessKey,
- });
- }
-
- const roleArn = config.roleArn;
- if (roleArn) {
- return new aws.ChainableTemporaryCredentials({
- masterCredentials: explicitCredentials,
- params: {
- RoleArn: roleArn,
- RoleSessionName: roleSessionName,
- ExternalId: config.externalId,
- },
- });
- }
-
- return explicitCredentials;
- }
-}
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts
index ecf3b1a961..6cec6ed66d 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts
@@ -15,24 +15,28 @@
*/
import { AwsEKSClusterProcessor } from './AwsEKSClusterProcessor';
-import AWSMock from 'aws-sdk-mock';
-import aws from 'aws-sdk';
+import { mockClient } from 'aws-sdk-client-mock';
+import {
+ ListClustersResponse,
+ DescribeClusterResponse,
+ ListClustersCommand,
+ DescribeClusterCommand,
+ EKSClient,
+} from '@aws-sdk/client-eks';
describe('AwsEKSClusterProcessor', () => {
- AWSMock.setSDKInstance(aws);
-
describe('readLocation', () => {
const processor = new (AwsEKSClusterProcessor as any)({});
const location = { type: 'aws-eks', target: '957140518395/us-west-2' };
const emit = jest.fn();
it('generates cluster correctly', async () => {
- const clusters: aws.EKS.Types.ListClustersResponse = {
+ const clusters: ListClustersResponse = {
clusters: ['backstage-test'],
nextToken: undefined,
};
- const cluster: aws.EKS.Types.DescribeClusterResponse = {
+ const cluster: DescribeClusterResponse = {
cluster: {
name: 'backstage-test',
arn: 'arn:aws:1',
@@ -42,9 +46,10 @@ describe('AwsEKSClusterProcessor', () => {
},
},
};
+ const mock = mockClient(EKSClient);
- AWSMock.mock('EKS', 'listClusters', clusters);
- AWSMock.mock('EKS', 'describeCluster', cluster);
+ mock.on(ListClustersCommand).resolves(clusters);
+ mock.on(DescribeClusterCommand).resolves(cluster);
await processor.readLocation(location, false, emit);
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts
index 05c85c7426..4a19b9eb7f 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts
@@ -24,8 +24,14 @@ import {
ANNOTATION_KUBERNETES_API_SERVER_CA,
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
} from '@backstage/plugin-kubernetes-common';
-import { Credentials, EKS } from 'aws-sdk';
+import { EKS } from '@aws-sdk/client-eks';
import { AWSCredentialFactory } from '../types';
+import { AwsCredentialIdentity, Provider } from '@aws-sdk/types';
+import {
+ AwsCredentialsManager,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
+import { Config } from '@backstage/config';
const ACCOUNTID_ANNOTATION: string = 'amazonaws.com/account-id';
const ARN_ANNOTATION: string = 'amazonaws.com/arn';
@@ -39,9 +45,22 @@ const ARN_ANNOTATION: string = 'amazonaws.com/arn';
*/
export class AwsEKSClusterProcessor implements CatalogProcessor {
private credentialsFactory?: AWSCredentialFactory;
+ private credentialsManager?: AwsCredentialsManager;
- constructor(options: { credentialsFactory?: AWSCredentialFactory }) {
+ static fromConfig(configRoot: Config): AwsEKSClusterProcessor {
+ const awsCredentaislManager =
+ DefaultAwsCredentialsManager.fromConfig(configRoot);
+ return new AwsEKSClusterProcessor({
+ credentialsManager: awsCredentaislManager,
+ });
+ }
+
+ constructor(options: {
+ credentialsFactory?: AWSCredentialFactory;
+ credentialsManager?: AwsCredentialsManager;
+ }) {
this.credentialsFactory = options.credentialsFactory;
+ this.credentialsManager = options.credentialsManager;
}
getProcessorName(): string {
@@ -73,20 +92,30 @@ export class AwsEKSClusterProcessor implements CatalogProcessor {
);
}
- let credentials: Credentials | undefined;
+ let credentials: AwsCredentialIdentity | undefined;
if (this.credentialsFactory) {
credentials = await this.credentialsFactory(accountId);
}
- const eksClient = new EKS({ credentials, region });
- const clusters = await eksClient.listClusters({}).promise();
+ let providerFunction: (() => Provider) | undefined;
+ if (this.credentialsManager) {
+ const credentialsProvider =
+ await this.credentialsManager.getCredentialProvider({ accountId });
+ providerFunction = () => credentialsProvider.sdkCredentialProvider;
+ }
+
+ const eksClient = new EKS({
+ credentials,
+ credentialDefaultProvider: providerFunction,
+ });
+ const clusters = await eksClient.listClusters({});
if (clusters.clusters === undefined) {
return true;
}
const results = clusters.clusters
- .map(cluster => eksClient.describeCluster({ name: cluster }).promise())
+ .map(cluster => eksClient.describeCluster({ name: cluster }))
.map(async describedClusterPromise => {
const describedCluster = await describedClusterPromise;
if (describedCluster.cluster) {
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts
index b9b8ba7741..458a1f3531 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts
@@ -14,7 +14,13 @@
* limitations under the License.
*/
+import { mockClient } from 'aws-sdk-client-mock';
+import 'aws-sdk-client-mock-jest';
import { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor';
+import {
+ ListAccountsCommand,
+ OrganizationsClient,
+} from '@aws-sdk/client-organizations';
describe('AwsOrganizationCloudAccountProcessor', () => {
describe('readLocation', () => {
@@ -23,26 +29,21 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
});
const location = { type: 'aws-cloud-accounts', target: '' };
const emit = jest.fn();
- const listAccounts = jest.fn();
+ const mock = mockClient(OrganizationsClient);
- processor.organizations.listAccounts = listAccounts;
- afterEach(() => jest.resetAllMocks());
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
it('generates component entities for accounts', async () => {
- listAccounts.mockImplementation(() => {
- return {
- async promise() {
- return {
- Accounts: [
- {
- Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
- Name: 'testaccount',
- },
- ],
- NextToken: undefined,
- };
+ mock.on(ListAccountsCommand).resolves({
+ Accounts: [
+ {
+ Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
+ Name: 'testaccount',
},
- };
+ ],
+ NextToken: undefined,
});
await processor.readLocation(location, false, emit);
expect(emit).toHaveBeenCalledWith({
@@ -74,24 +75,18 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
type: 'aws-cloud-accounts',
target: 'o-1vl18kc5a3',
};
- listAccounts.mockImplementation(() => {
- return {
- async promise() {
- return {
- Accounts: [
- {
- Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
- Name: 'testaccount',
- },
- {
- Arn: 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395',
- Name: 'testaccount2',
- },
- ],
- NextToken: undefined,
- };
+ mock.on(ListAccountsCommand).resolves({
+ Accounts: [
+ {
+ Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
+ Name: 'testaccount',
},
- };
+ {
+ Arn: 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395',
+ Name: 'testaccount2',
+ },
+ ],
+ NextToken: undefined,
});
await processor.readLocation(locationTest, false, emit);
expect(emit).toHaveBeenCalledTimes(1);
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts
index c0ba1e3e98..264fa7a50d 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts
@@ -22,13 +22,17 @@ import {
processingResult,
} from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
-import AWS, { Credentials, Organizations } from 'aws-sdk';
-import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations';
-import { Logger } from 'winston';
import {
- AwsOrganizationProviderConfig,
- readAwsOrganizationConfig,
-} from '../awsOrganization/config';
+ Account,
+ ListAccountsResponse,
+ Organizations,
+} from '@aws-sdk/client-organizations';
+import { Logger } from 'winston';
+import { readAwsOrganizationConfig } from '../awsOrganization/config';
+import {
+ AwsCredentialProvider,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
const AWS_ORGANIZATION_REGION = 'us-east-1';
const LOCATION_TYPE = 'aws-cloud-accounts';
@@ -47,39 +51,31 @@ const ORGANIZATION_ANNOTATION = 'amazonaws.com/organization-id';
*/
export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
private readonly organizations: Organizations;
- private readonly provider: AwsOrganizationProviderConfig;
+ private readonly logger: Logger;
- static fromConfig(config: Config, options: { logger: Logger }) {
+ static async fromConfig(config: Config, options: { logger: Logger }) {
const c = config.getOptionalConfig('catalog.processors.awsOrganization');
- return new AwsOrganizationCloudAccountProcessor({
- ...options,
- provider: c ? readAwsOrganizationConfig(c) : {},
+ const orgConfig = c ? readAwsOrganizationConfig(c) : undefined;
+ const awsCredentialsManager =
+ DefaultAwsCredentialsManager.fromConfig(config);
+ const credProvider = await awsCredentialsManager.getCredentialProvider({
+ arn: orgConfig?.roleArn,
});
- }
-
- private static buildCredentials(
- config: AwsOrganizationProviderConfig,
- ): Credentials | undefined {
- const roleArn = config.roleArn;
- if (!roleArn) {
- return undefined;
- }
-
- return new AWS.ChainableTemporaryCredentials({
- params: {
- RoleSessionName: 'backstage-aws-organization-processor',
- RoleArn: roleArn,
- },
- });
- }
-
- private constructor(options: { provider: AwsOrganizationProviderConfig }) {
- this.provider = options.provider;
- const credentials = AwsOrganizationCloudAccountProcessor.buildCredentials(
- this.provider,
+ return new AwsOrganizationCloudAccountProcessor(
+ credProvider,
+ options.logger,
);
- this.organizations = new AWS.Organizations({
- credentials,
+ }
+
+ private constructor(
+ private readonly credProvider: AwsCredentialProvider,
+ logger: Logger,
+ ) {
+ this.logger = logger?.child({
+ target: this.getProcessorName(),
+ });
+ this.organizations = new Organizations({
+ credentialDefaultProvider: () => this.credProvider.sdkCredentialProvider,
region: AWS_ORGANIZATION_REGION,
}); // Only available in us-east-1
}
@@ -97,6 +93,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
return false;
}
+ this.logger?.info('Discovering AWS Organization Account objects');
+
(await this.getAwsAccounts())
.map(account => this.mapAccountToComponent(account))
.filter(entity => {
@@ -143,9 +141,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
let nextToken = undefined;
while (isInitialAttempt || nextToken) {
isInitialAttempt = false;
- const orgAccounts: ListAccountsResponse = await this.organizations
- .listAccounts({ NextToken: nextToken })
- .promise();
+ const orgAccounts: ListAccountsResponse =
+ await this.organizations.listAccounts({ NextToken: nextToken });
if (orgAccounts.Accounts) {
awsAccounts = awsAccounts.concat(orgAccounts.Accounts);
}
diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts
index 821fb6d4f4..e9e17ad76a 100644
--- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts
+++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts
@@ -23,8 +23,9 @@ import {
import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { AwsS3EntityProvider } from './AwsS3EntityProvider';
-import aws from 'aws-sdk';
-import AWSMock from 'aws-sdk-mock';
+import { mockClient } from 'aws-sdk-client-mock';
+import 'aws-sdk-client-mock-jest';
+import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';
class PersistingTaskRunner implements TaskRunner {
private tasks: TaskInvocationDefinition[] = [];
@@ -42,38 +43,39 @@ class PersistingTaskRunner implements TaskRunner {
const logger = getVoidLogger();
describe('AwsS3EntityProvider', () => {
- AWSMock.setSDKInstance(aws);
- const createObjectList = (keys: string[]): aws.S3.ObjectList => {
+ const createObjectList = (keys: string[]) => {
const objects = keys.map(key => {
return {
Key: key,
- } as aws.S3.Types.Object;
+ };
});
-
- return objects as aws.S3.ObjectList;
+ return objects;
};
const keys = ['key1.yaml', 'key2.yaml', 'key3.yaml', 'key 4.yaml'];
+ const mock = mockClient(S3Client);
- AWSMock.mock('S3', 'listObjectsV2', async req => {
- const prefix = req.Prefix ?? '';
+ beforeEach(() => {
+ mock.on(ListObjectsV2Command).callsFake(async req => {
+ const prefix = req.Prefix ?? '';
+
+ if (!req.ContinuationToken) {
+ return {
+ Contents: createObjectList(
+ keys
+ .slice(0, Math.ceil(keys.length / 2))
+ .map(key => `${prefix}${key}`),
+ ),
+ NextContinuationToken: 'next-token',
+ };
+ }
- if (!req.ContinuationToken) {
return {
Contents: createObjectList(
- keys
- .slice(0, Math.ceil(keys.length / 2))
- .map(key => `${prefix}${key}`),
+ keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`),
),
- NextContinuationToken: 'next-token',
- } as aws.S3.Types.ListObjectsV2Output;
- }
-
- return {
- Contents: createObjectList(
- keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`),
- ),
- } as aws.S3.Types.ListObjectsV2Output;
+ };
+ });
});
afterEach(() => {
@@ -103,7 +105,9 @@ describe('AwsS3EntityProvider', () => {
});
const schedulingConfig: Record = {};
-
+ const normalizedExpectedBaseUrl = expectedBaseUrl.endsWith('/')
+ ? expectedBaseUrl
+ : `${expectedBaseUrl}/`;
const schedule = new PersistingTaskRunner();
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
@@ -131,7 +135,7 @@ describe('AwsS3EntityProvider', () => {
await (taskDef.fn as () => Promise)();
const expectedEntities = keys.map(key => {
- const url = encodeURI(`${expectedBaseUrl}${key}`);
+ const url = encodeURI(`${normalizedExpectedBaseUrl}${key}`);
return {
entity: {
apiVersion: 'backstage.io/v1alpha1',
@@ -161,6 +165,25 @@ describe('AwsS3EntityProvider', () => {
// eslint-disable-next-line jest/expect-expect
it('apply full update on scheduled execution', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket-1',
+ prefix: 'sub/dir/',
+ region: 'eu-west-1',
+ },
+ 'https://bucket-1.s3.eu-west-1.amazonaws.com/sub/dir/',
+ {
+ 'key1.yaml': 'generated-8ece85ad90200c6577b99f553dcbedde05fa34bb',
+ 'key2.yaml': 'generated-6b54c6aaa44696f5e91ce0f54fb27bf837549d11',
+ 'key3.yaml': 'generated-88c703cf1aa66913db4033b029adc0b174574646',
+ 'key 4.yaml': 'generated-2b7e068bb4ec818c14f179a1e721843fc2dbc5f9',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('apply full update on scheduled execution force path style', async () => {
return expectMutation(
'regionalStatic',
{
@@ -175,11 +198,72 @@ describe('AwsS3EntityProvider', () => {
'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d',
'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0',
},
+ {
+ s3ForcePathStyle: true,
+ },
);
});
// eslint-disable-next-line jest/expect-expect
- it('us-east-1 has region-less URLs', async () => {
+ it('apply full update on scheduled execution no prefix', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket-1',
+ region: 'eu-west-1',
+ },
+ 'https://bucket-1.s3.eu-west-1.amazonaws.com/',
+ {
+ 'key1.yaml': 'generated-ac6d99e56e0fbd6bf3719a5182f37bfa84b1cddc',
+ 'key2.yaml': 'generated-cc3e1145496cd95bf3898687bb5b7d2991a7b7fd',
+ 'key3.yaml': 'generated-a197557b0cb89d14ac1297231ed930bd4c40b664',
+ 'key 4.yaml': 'generated-a774e6c1d1c7ee511b7d79ebb99b08a39cf439c4',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('apply full update on scheduled execution force path style no prefix', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket-1',
+ region: 'eu-west-1',
+ },
+ 'https://s3.eu-west-1.amazonaws.com/bucket-1',
+ {
+ 'key1.yaml': 'generated-b390cd4de7ff00f6d51426870fc0e685029fbf09',
+ 'key2.yaml': 'generated-15ebeb1e53ffe71ea93f037076aefcdc5cc5e617',
+ 'key3.yaml': 'generated-fd0a33e70bafd707a60139decbd0383361ab3b4e',
+ 'key 4.yaml': 'generated-ac574fb266d9c8bd983b25a208c538e4ad2c1d06',
+ },
+ {
+ s3ForcePathStyle: true,
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('apply full update on scheduled execution bucket with dot in it', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket.1',
+ prefix: 'sub/dir/',
+ region: 'eu-west-1',
+ },
+ 'https://s3.eu-west-1.amazonaws.com/bucket.1/sub/dir/',
+ {
+ 'key1.yaml': 'generated-651b5851d4670b7d5bb9a319d8f6eb295092e4ad',
+ 'key2.yaml': 'generated-ccddcdc3ad814aea072883118f3a9d0ac39a161e',
+ 'key3.yaml': 'generated-b2b8548903aa764544ab79dc81ab5093e6e4ba83',
+ 'key 4.yaml': 'generated-aafa9cc0422f5e8ccce1616dc0c74fa94f692af3',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('us-east-1 does not have region-less URLs', async () => {
return expectMutation(
'usEast1',
{
@@ -187,12 +271,12 @@ describe('AwsS3EntityProvider', () => {
prefix: 'sub/dir/',
region: 'us-east-1',
},
- 'https://s3.amazonaws.com/bucket-1/sub/dir/',
+ 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/',
{
- 'key1.yaml': 'generated-f7e3f1c89f62cbf0d82db16452faaa7c040fc331',
- 'key2.yaml': 'generated-925173f7dba1acaa73cac5ef4d10c3ed7660aa25',
- 'key3.yaml': 'generated-d94cf017911ed7fb3be4a62a3dae1f5202879de3',
- 'key 4.yaml': 'generated-f917fca0cfacc2be478ca2a3cff94d00917a1cde',
+ 'key1.yaml': 'generated-980e6ad47fbfbfeead708a9c7c87331b7540296a',
+ 'key2.yaml': 'generated-266794d8e789089dddba2b42cd79e70b149aa61c',
+ 'key3.yaml': 'generated-96f0cdcd7e33aa687c19d160ec7d5b1975cb9ea1',
+ 'key 4.yaml': 'generated-4f6d3ffb232e4a5b82c3a52549e4c36dab1c2f8d',
},
);
});
@@ -209,18 +293,60 @@ describe('AwsS3EntityProvider', () => {
bucketName: 'bucket-1',
prefix: 'sub/dir/',
},
- 'https://s3.eu-central-1.amazonaws.com/bucket-1/sub/dir/',
+ 'https://bucket-1.s3.eu-central-1.amazonaws.com/sub/dir/',
{
- 'key1.yaml': 'generated-285d144b5c1e24e801b97f61f794d566dabc7236',
- 'key2.yaml': 'generated-36b78d7fa69690059797d4cc4f40c2a5eaed1d6d',
- 'key3.yaml': 'generated-a39ad804bf65993cbc0f3810ff128a3c91daf768',
- 'key 4.yaml': 'generated-bd7622ac86c313cb812d90e335b2f5f6bff14670',
+ 'key1.yaml': 'generated-bb17a9f9b420edd93137692f000b2d443a29239c',
+ 'key2.yaml': 'generated-b4559ecc8b615407e421dbaa3fcb9338e977b8ac',
+ 'key3.yaml': 'generated-bb3d500a6f8b351b0eed1aec77ebce65c1ab41fb',
+ 'key 4.yaml': 'generated-686de87f5882f6558b43280c5b6047e485199e88',
},
);
});
// eslint-disable-next-line jest/expect-expect
it('custom endpoint', async () => {
+ return expectMutation(
+ 'customEndpoint',
+ {
+ bucketName: 'bucket-1',
+ prefix: 'sub/dir/',
+ },
+ 'http://bucket-1.localhost:1234/sub/dir/',
+ {
+ 'key1.yaml': 'generated-79625971ead9c39c91b0bfef4d7c748325e31234',
+ 'key2.yaml': 'generated-eeae95f16605d3afdc615436c18c9fbd35781299',
+ 'key3.yaml': 'generated-692976561563c3fa55889ad090363716c7b7e915',
+ 'key 4.yaml': 'generated-c8f92ea0eb96c355b7e56cbc461cef29f830747e',
+ },
+ {
+ endpoint: 'http://localhost:1234/',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('custom endpoint bucket dot', async () => {
+ return expectMutation(
+ 'customEndpoint',
+ {
+ bucketName: 'bucket.1',
+ prefix: 'sub/dir/',
+ },
+ 'http://localhost:1234/bucket.1/sub/dir/',
+ {
+ 'key1.yaml': 'generated-07be77ac8626ac76a924414e0edf5ce66d3e0b27',
+ 'key2.yaml': 'generated-47295ada3c894cf9bac0d570a34d909244ea2f74',
+ 'key3.yaml': 'generated-30e98f3ebd350f3b618acd024199b5f31545495d',
+ 'key 4.yaml': 'generated-b71bc7661b92cef0db62c9652912fd5e4b245aa7',
+ },
+ {
+ endpoint: 'http://localhost:1234/',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('custom endpoint force path style', async () => {
return expectMutation(
'customEndpoint',
{
@@ -235,7 +361,8 @@ describe('AwsS3EntityProvider', () => {
'key 4.yaml': 'generated-659f17a2429f107db04850f9081e3e9c56084aeb',
},
{
- endpoint: 'http://localhost:1234',
+ endpoint: 'http://localhost:1234/',
+ s3ForcePathStyle: true,
},
);
});
@@ -305,12 +432,12 @@ describe('AwsS3EntityProvider', () => {
},
},
},
- 'https://s3.eu-west-1.amazonaws.com/bucket-1/sub/dir/',
+ 'https://bucket-1.s3.eu-west-1.amazonaws.com/sub/dir/',
{
- 'key1.yaml': 'generated-7f6d5861b0b3401a38b5fe62e6c7ca11da5fd6d8',
- 'key2.yaml': 'generated-a290be145586042af7d80715626399c9d661718d',
- 'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d',
- 'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0',
+ 'key1.yaml': 'generated-8ece85ad90200c6577b99f553dcbedde05fa34bb',
+ 'key2.yaml': 'generated-6b54c6aaa44696f5e91ce0f54fb27bf837549d11',
+ 'key3.yaml': 'generated-88c703cf1aa66913db4033b029adc0b174574646',
+ 'key 4.yaml': 'generated-2b7e068bb4ec818c14f179a1e721843fc2dbc5f9',
},
undefined,
true,
diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts
index 8212f9279a..4a87a42e75 100644
--- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts
+++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts
@@ -23,13 +23,20 @@ import {
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
-import { AwsCredentials } from '../credentials/AwsCredentials';
import { readAwsS3Configs } from './config';
import { AwsS3Config } from './types';
-import { S3 } from 'aws-sdk';
-import { ListObjectsV2Output } from 'aws-sdk/clients/s3';
+import {
+ ListObjectsV2Command,
+ ListObjectsV2Output,
+ S3,
+} from '@aws-sdk/client-s3';
import * as uuid from 'uuid';
import { Logger } from 'winston';
+import { getEndpointFromInstructions } from '@aws-sdk/middleware-endpoint';
+import {
+ AwsCredentialsManager,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
// TODO: event-based updates using S3 events (+ queue like SQS)?
/**
@@ -41,9 +48,10 @@ import { Logger } from 'winston';
*/
export class AwsS3EntityProvider implements EntityProvider {
private readonly logger: Logger;
- private readonly s3: S3;
+ private s3?: S3;
private readonly scheduleFn: () => Promise;
private connection?: EntityProviderConnection;
+ private endpoint?: string;
static fromConfig(
configRoot: Config,
@@ -78,7 +86,8 @@ export class AwsS3EntityProvider implements EntityProvider {
`No schedule provided neither via code nor config for awsS3-provider:${providerConfig.id}.`,
);
}
-
+ const awsCredentialsManager =
+ DefaultAwsCredentialsManager.fromConfig(configRoot);
const taskRunner =
options.schedule ??
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
@@ -86,6 +95,7 @@ export class AwsS3EntityProvider implements EntityProvider {
return new AwsS3EntityProvider(
providerConfig,
integration,
+ awsCredentialsManager,
options.logger,
taskRunner,
);
@@ -94,7 +104,8 @@ export class AwsS3EntityProvider implements EntityProvider {
private constructor(
private readonly config: AwsS3Config,
- integration: AwsS3Integration,
+ private readonly integration: AwsS3Integration,
+ private readonly awsCredentialsManager: AwsCredentialsManager,
logger: Logger,
taskRunner: TaskRunner,
) {
@@ -102,17 +113,6 @@ export class AwsS3EntityProvider implements EntityProvider {
target: this.getProviderName(),
});
- this.s3 = new S3({
- apiVersion: '2006-03-01',
- credentials: AwsCredentials.create(
- integration.config,
- 'backstage-aws-s3-provider',
- ),
- endpoint: integration.config.endpoint,
- region: this.config.region,
- s3ForcePathStyle: integration.config.s3ForcePathStyle,
- });
-
this.scheduleFn = this.createScheduleFn(taskRunner);
}
@@ -146,6 +146,28 @@ export class AwsS3EntityProvider implements EntityProvider {
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
async connect(connection: EntityProviderConnection): Promise {
this.connection = connection;
+ const credProvider =
+ await this.awsCredentialsManager.getCredentialProvider();
+ this.s3 = new S3({
+ apiVersion: '2006-03-01',
+ credentialDefaultProvider: () => credProvider.sdkCredentialProvider,
+ endpoint: this.integration.config.endpoint,
+ region: this.config.region,
+ forcePathStyle: this.integration.config.s3ForcePathStyle,
+ });
+
+ // https://github.com/aws/aws-sdk-js-v3/issues/4122#issuecomment-1298968804
+ const endpoint = await getEndpointFromInstructions(
+ {
+ Bucket: this.config.bucketName,
+ },
+ ListObjectsV2Command,
+ this.s3.config as unknown as Record,
+ );
+ if (endpoint?.url)
+ this.endpoint = endpoint.url.href.endsWith('/')
+ ? endpoint.url.href
+ : `${endpoint.url.href}/`;
await this.scheduleFn();
}
@@ -175,18 +197,21 @@ export class AwsS3EntityProvider implements EntityProvider {
}
private async listAllObjectKeys(): Promise {
+ if (!this.s3) {
+ throw new Error('Not initialized');
+ }
+
const keys: string[] = [];
let continuationToken: string | undefined = undefined;
let output: ListObjectsV2Output;
do {
- const request = this.s3.listObjectsV2({
+ output = await this.s3.listObjectsV2({
Bucket: this.config.bucketName,
ContinuationToken: continuationToken,
Prefix: this.config.prefix,
});
- output = await request.promise();
if (output.Contents) {
output.Contents.forEach(item => {
if (item.Key && !item.Key.endsWith('/')) {
@@ -209,9 +234,6 @@ export class AwsS3EntityProvider implements EntityProvider {
}
private createObjectUrl(key: string): string {
- const bucketName = this.config.bucketName;
- const endpoint = this.s3.endpoint.href;
-
- return encodeURI(`${endpoint}${bucketName}/${key}`);
+ return new URL(key, this.endpoint).href;
}
}
diff --git a/plugins/catalog-backend-module-aws/src/types.ts b/plugins/catalog-backend-module-aws/src/types.ts
index 3d9df38181..23460c104e 100644
--- a/plugins/catalog-backend-module-aws/src/types.ts
+++ b/plugins/catalog-backend-module-aws/src/types.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Credentials } from 'aws-sdk';
+import { AwsCredentialIdentity } from '@aws-sdk/types';
/**
* A factory for providing user-specified AWS credentials for a given AWS account.
@@ -22,4 +22,4 @@ import { Credentials } from 'aws-sdk';
*/
export type AWSCredentialFactory = (
awsAccountId: string,
-) => Promise;
+) => Promise;
diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md
index f54a55f9e9..9e3b24071f 100644
--- a/plugins/catalog-backend-module-github/CHANGELOG.md
+++ b/plugins/catalog-backend-module-github/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-catalog-backend-module-github
+## 0.3.0-next.2
+
+### Minor Changes
+
+- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider`
+
+ **BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
+
+### Patch Changes
+
+- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
## 0.2.8-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md
index 0698fae8a0..7b944b33a9 100644
--- a/plugins/catalog-backend-module-github/api-report.md
+++ b/plugins/catalog-backend-module-github/api-report.md
@@ -10,6 +10,7 @@ import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
+import { EventBroker } from '@backstage/plugin-events-node';
import { EventParams } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { GithubCredentialsProvider } from '@backstage/integration';
@@ -158,6 +159,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
// @public
export interface GithubMultiOrgEntityProviderOptions {
+ eventBroker?: EventBroker;
githubCredentialsProvider?: GithubCredentialsProvider;
githubUrl: string;
id: string;
diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json
index 5b29baa05c..1f79fea531 100644
--- a/plugins/catalog-backend-module-github/package.json
+++ b/plugins/catalog-backend-module-github/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-github",
"description": "A Backstage catalog backend module that helps integrate towards GitHub",
- "version": "0.2.8-next.1",
+ "version": "0.3.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts
index df4565a174..7a0a29e098 100644
--- a/plugins/catalog-backend-module-github/src/lib/github.test.ts
+++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts
@@ -21,6 +21,7 @@ import { graphql as graphqlMsw } from 'msw';
import { setupServer } from 'msw/node';
import { TeamTransformer, UserTransformer } from './defaultTransformers';
import {
+ getOrganizationsFromUser,
getOrganizationTeams,
getOrganizationUsers,
getTeamMembers,
@@ -448,6 +449,37 @@ describe('github', () => {
});
});
+ describe('getOrganizationsFromUser', () => {
+ it('reads orgs from user', async () => {
+ const input: QueryResponse = {
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'a',
+ },
+ {
+ login: 'b',
+ },
+ {
+ login: 'c',
+ },
+ ],
+ },
+ },
+ };
+
+ server.use(
+ graphqlMsw.query('orgs', (_req, res, ctx) => res(ctx.data(input))),
+ );
+
+ await expect(getOrganizationsFromUser(graphql, 'foo')).resolves.toEqual({
+ orgs: ['a', 'b', 'c'],
+ });
+ });
+ });
+
describe('getTeamMembers', () => {
it('reads team members', async () => {
const input: QueryResponse = {
diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts
index d0b629092a..412b751c38 100644
--- a/plugins/catalog-backend-module-github/src/lib/github.ts
+++ b/plugins/catalog-backend-module-github/src/lib/github.ts
@@ -33,6 +33,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node';
export type QueryResponse = {
organization?: OrganizationResponse;
repositoryOwner?: RepositoryOwnerResponse;
+ user?: UserResponse;
};
type RepositoryOwnerResponse = {
@@ -46,11 +47,19 @@ export type OrganizationResponse = {
repositories?: Connection;
};
+export type UserResponse = {
+ organizations?: Connection;
+};
+
export type PageInfo = {
hasNextPage: boolean;
endCursor?: string;
};
+export type GithubOrg = {
+ login: string;
+};
+
/**
* Github User
*
@@ -334,6 +343,34 @@ export async function getOrganizationTeamsFromUsers(
return { groups };
}
+export async function getOrganizationsFromUser(
+ client: typeof graphql,
+ user: string,
+): Promise<{
+ orgs: string[];
+}> {
+ const query = `
+ query orgs($user: String!) {
+ user(login: $user) {
+ organizations(first: 100) {
+ nodes { login }
+ pageInfo { hasNextPage, endCursor }
+ }
+ }
+ }`;
+
+ const orgs = await queryWithPaging(
+ client,
+ query,
+ '',
+ r => r.user?.organizations,
+ async o => o.login,
+ { user },
+ );
+
+ return { orgs };
+}
+
export async function getOrganizationTeam(
client: typeof graphql,
org: string,
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts
index 76ab55552d..99bef6e074 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts
@@ -16,11 +16,13 @@
import { getVoidLogger } from '@backstage/backend-common';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import { ConfigReader } from '@backstage/config';
import {
GithubCredentialsProvider,
GithubIntegrationConfig,
} from '@backstage/integration';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
+import { EventSubscriber } from '@backstage/plugin-events-node';
import { graphql } from '@octokit/graphql';
import {
GithubMultiOrgEntityProvider,
@@ -160,7 +162,7 @@ describe('GithubMultiOrgEntityProvider', () => {
const logger = getVoidLogger();
const gitHubConfig: GithubIntegrationConfig = {
- host: 'https://github.com',
+ host: 'github.com',
};
const mockGetCredentials = jest.fn().mockReturnValue({
@@ -201,9 +203,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
@@ -227,9 +229,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'github.com/user-login': 'x',
},
description: 'z',
@@ -253,9 +255,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'github.com/user-login': 'q',
},
description: 's',
@@ -279,9 +281,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgA/teams/team',
+ 'url:https://github.com/orgs/orgA/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgA/teams/team',
+ 'url:https://github.com/orgs/orgA/teams/team',
'github.com/team-slug': 'orgA/team',
},
namespace: 'orga',
@@ -308,9 +310,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgB/teams/team',
+ 'url:https://github.com/orgs/orgB/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgB/teams/team',
+ 'url:https://github.com/orgs/orgB/teams/team',
'github.com/team-slug': 'orgB/team',
},
namespace: 'orgb',
@@ -467,7 +469,7 @@ describe('GithubMultiOrgEntityProvider', () => {
const logger = getVoidLogger();
const gitHubConfig: GithubIntegrationConfig = {
- host: 'https://github.com',
+ host: 'github.com',
};
const mockGetCredentials = jest.fn().mockReturnValue({
@@ -507,9 +509,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
@@ -533,9 +535,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'github.com/user-login': 'x',
},
description: 'z',
@@ -559,9 +561,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'github.com/user-login': 'q',
},
description: 's',
@@ -585,9 +587,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgC/teams/team',
+ 'url:https://github.com/orgs/orgC/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgC/teams/team',
+ 'url:https://github.com/orgs/orgC/teams/team',
'github.com/team-slug': 'orgC/team',
},
namespace: 'orgc',
@@ -614,9 +616,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgD/teams/team',
+ 'url:https://github.com/orgs/orgD/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgD/teams/team',
+ 'url:https://github.com/orgs/orgD/teams/team',
'github.com/team-slug': 'orgD/team',
},
namespace: 'orgd',
@@ -713,4 +715,1223 @@ describe('GithubMultiOrgEntityProvider', () => {
});
});
});
+
+ describe('events', () => {
+ let onEvent: Function;
+
+ const entityProviderConnection: EntityProviderConnection = {
+ applyMutation: jest.fn(),
+ refresh: jest.fn(),
+ };
+
+ beforeEach(() => {
+ const logger = getVoidLogger();
+ const config = new ConfigReader({
+ integrations: {
+ github: [
+ {
+ host: 'github.com',
+ },
+ ],
+ },
+ });
+
+ const mockGetCredentials = jest.fn().mockReturnValue({
+ headers: { token: 'blah' },
+ type: 'app',
+ });
+
+ const githubCredentialsProvider: GithubCredentialsProvider = {
+ getCredentials: mockGetCredentials,
+ };
+
+ const mockEventBroker = {
+ publish: async () => {},
+ subscribe: (subscriber: EventSubscriber) => {
+ onEvent = subscriber.onEvent;
+ },
+ };
+
+ const entityProvider = GithubMultiOrgEntityProvider.fromConfig(config, {
+ id: 'my-id',
+ githubCredentialsProvider,
+ githubUrl: 'https://github.com',
+ logger,
+ orgs: ['orgA', 'orgB'],
+ eventBroker: mockEventBroker,
+ });
+
+ entityProvider.connect(entityProviderConnection);
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('should ignore events from non-applicable orgs', async () => {
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_added',
+ membership: {
+ user: {
+ name: 'githubuser',
+ login: 'githubuser',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ },
+ },
+ organization: {
+ login: 'test-org',
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled();
+
+ await onEvent({
+ topic: 'github.installation',
+ eventPayload: {
+ action: 'created',
+ installation: {
+ account: {
+ login: 'test-org',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled();
+ });
+
+ describe('installation', () => {
+ it('adds users and groups from new org', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ membersWithRole: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'a',
+ name: 'b',
+ bio: 'c',
+ email: 'd',
+ avatarUrl: 'e',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ parentTeam: {
+ slug: 'parent',
+ combinedSlug: '',
+ members: { pageInfo: { hasNextPage: false }, nodes: [] },
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ parentTeam: {
+ slug: 'parent',
+ combinedSlug: '',
+ members: { pageInfo: { hasNextPage: false }, nodes: [] },
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ parentTeam: {
+ slug: 'parent',
+ combinedSlug: '',
+ members: { pageInfo: { hasNextPage: false }, nodes: [] },
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.installation',
+ eventPayload: {
+ action: 'created',
+ installation: {
+ account: {
+ login: 'orgB',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ description: 'c',
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team', 'orgb/team'],
+ profile: {
+ displayName: 'b',
+ email: 'd',
+ picture: 'e',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgB/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgB/teams/team',
+ 'github.com/team-slug': 'orgB/team',
+ },
+ namespace: 'orgb',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamB',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: ['default/a'],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+ });
+
+ describe('organization', () => {
+ it('should add a new user', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ {
+ login: 'orgB',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_added',
+ organization: {
+ login: 'orgB',
+ },
+ membership: {
+ user: {
+ name: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ login: 'a',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+
+ it('should remove a user', async () => {
+ const mockClient = jest.fn();
+
+ mockClient.mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgC',
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_removed',
+ organization: {
+ login: 'orgB',
+ },
+ membership: {
+ user: {
+ name: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ login: 'a',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [],
+ removed: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: [],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ });
+ });
+
+ it('should only update group memberships of a user that still belongs to an applicable org', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_removed',
+ organization: {
+ login: 'orgB',
+ },
+ membership: {
+ user: {
+ name: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ login: 'a',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+ });
+
+ describe('team', () => {
+ it('should create a new group from a new team', async () => {
+ await onEvent({
+ topic: 'github.team',
+ eventPayload: {
+ action: 'created',
+ organization: {
+ login: 'orgB',
+ },
+ team: {
+ name: 'New Team',
+ slug: 'new-team',
+ description: 'description from the new team',
+ html_url: 'https://github.com/orgs/orgB/teams/new-team',
+ parent: {
+ slug: 'father-team',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'new-team',
+ namespace: 'orgb',
+ description: 'description from the new team',
+ annotations: {
+ 'backstage.io/edit-url':
+ 'https://github.com/orgs/orgB/teams/new-team/edit',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'github.com/team-slug': 'orgB/new-team',
+ },
+ },
+ spec: {
+ type: 'team',
+ children: [],
+ members: [],
+ parent: 'father-team',
+ profile: {
+ displayName: 'New Team',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+
+ it('should remove a group from a deleted team', async () => {
+ await onEvent({
+ topic: 'github.team',
+ eventPayload: {
+ action: 'deleted',
+ organization: {
+ login: 'orgB',
+ },
+ team: {
+ name: 'New Team',
+ slug: 'new-team',
+ description: 'description from the new team',
+ html_url: 'https://github.com/orgs/orgB/teams/new-team',
+ parent: {
+ slug: 'father-team',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [],
+ removed: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'new-team',
+ namespace: 'orgb',
+ description: 'description from the new team',
+ annotations: {
+ 'backstage.io/edit-url':
+ 'https://github.com/orgs/orgB/teams/new-team/edit',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'github.com/team-slug': 'orgB/new-team',
+ },
+ },
+ spec: {
+ type: 'team',
+ children: [],
+ members: [],
+ parent: 'father-team',
+ profile: {
+ displayName: 'New Team',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ });
+ });
+
+ it('should update group and user entities when a team is edited', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ team: {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ membersWithRole: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'a',
+ name: 'b',
+ bio: 'c',
+ email: 'd',
+ avatarUrl: 'e',
+ },
+ {
+ login: 'w',
+ name: 'x',
+ bio: 'y',
+ email: 'z',
+ avatarUrl: 'q',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.team',
+ eventPayload: {
+ action: 'edited',
+ changes: {
+ name: {
+ from: 'oldName',
+ },
+ description: {
+ from: 'oldDescription',
+ },
+ },
+ team: {
+ slug: 'team',
+ parent: {
+ slug: 'parent',
+ },
+ },
+ organization: {
+ login: 'orgA',
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ description: 'c',
+ },
+ spec: {
+ memberOf: ['orga/team', 'orgb/team'],
+ profile: {
+ displayName: 'b',
+ email: 'd',
+ picture: 'e',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/edit-url': 'https://example.com',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'github.com/team-slug': 'orgA/team',
+ },
+ namespace: 'orga',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamA',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: ['default/a'],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/oldname',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/oldname',
+ 'github.com/team-slug': 'orgA/oldname',
+ },
+ namespace: 'orga',
+ name: 'oldname',
+ description: 'oldDescription',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'oldName',
+ },
+ type: 'team',
+ members: [],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ });
+ });
+ });
+
+ describe('membership', () => {
+ it('should update group and user entities when a member is added', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ team: {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ {
+ login: 'orgB',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.membership',
+ eventPayload: {
+ action: 'added',
+ organization: {
+ login: 'orgA',
+ },
+ team: {
+ slug: 'teama',
+ },
+ member: {
+ name: 'a',
+ login: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ },
+ },
+ });
+ await new Promise(process.nextTick);
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team', 'orgb/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/edit-url': 'https://example.com',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'github.com/team-slug': 'orgA/team',
+ },
+ namespace: 'orga',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamA',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: ['default/a'],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+
+ it('should update group and user entities when a member is removed', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ team: {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [],
+ },
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ {
+ login: 'orgB',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.membership',
+ eventPayload: {
+ action: 'removed',
+ organization: {
+ login: 'orgA',
+ },
+ team: {
+ slug: 'teama',
+ },
+ member: {
+ name: 'a',
+ login: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ },
+ },
+ });
+ await new Promise(process.nextTick);
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orgb/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/edit-url': 'https://example.com',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'github.com/team-slug': 'orgA/team',
+ },
+ namespace: 'orga',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamA',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: [],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+ });
+ });
});
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts
index ce45f30d96..51796c2116 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts
@@ -21,7 +21,9 @@ import {
DEFAULT_NAMESPACE,
Entity,
GroupEntity,
+ parseEntityRef,
stringifyEntityRef,
+ UserEntity,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
@@ -32,10 +34,24 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import {
+ DeferredEntity,
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
+import { EventBroker, EventParams } from '@backstage/plugin-events-node';
import { graphql } from '@octokit/graphql';
+import {
+ InstallationCreatedEvent,
+ InstallationEvent,
+ OrganizationEvent,
+ OrganizationMemberAddedEvent,
+ OrganizationMemberRemovedEvent,
+ MembershipEvent,
+ TeamCreatedEvent,
+ TeamDeletedEvent,
+ TeamEditedEvent,
+ TeamEvent,
+} from '@octokit/webhooks-types';
import { merge } from 'lodash';
import * as uuid from 'uuid';
import { Logger } from 'winston';
@@ -44,6 +60,7 @@ import {
assignGroupsToUsers,
buildOrgHierarchy,
defaultOrganizationTeamTransformer,
+ defaultUserTransformer,
getOrganizationTeams,
getOrganizationUsers,
GithubTeam,
@@ -55,6 +72,11 @@ import {
ANNOTATION_GITHUB_TEAM_SLUG,
ANNOTATION_GITHUB_USER_LOGIN,
} from '../lib/annotation';
+import {
+ getOrganizationsFromUser,
+ getOrganizationTeam,
+ getOrganizationTeamsFromUsers,
+} from '../lib/github';
import { splitTeamSlug } from '../lib/util';
/**
@@ -118,8 +140,18 @@ export interface GithubMultiOrgEntityProviderOptions {
* By default groups will be namespaced according to their GitHub org.
*/
teamTransformer?: TeamTransformer;
+
+ /**
+ * An EventBroker to subscribe this provider to GitHub events to trigger delta mutations
+ */
+ eventBroker?: EventBroker;
}
+type CreateDeltaOperation = (entities: Entity[]) => {
+ added: DeferredEntity[];
+ removed: DeferredEntity[];
+};
+
/**
* Ingests org data (users and groups) from GitHub.
*
@@ -161,6 +193,13 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
provider.schedule(options.schedule);
+ if (options.eventBroker) {
+ options.eventBroker.subscribe({
+ supportsEventTopics: provider.supportsEventTopics.bind(provider),
+ onEvent: provider.onEvent.bind(provider),
+ });
+ }
+
return provider;
}
@@ -271,6 +310,476 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
markCommitComplete();
}
+ private supportsEventTopics(): string[] {
+ return [
+ 'github.installation',
+ 'github.organization',
+ 'github.team',
+ 'github.membership',
+ ];
+ }
+
+ private async onEvent(params: EventParams): Promise {
+ const { logger } = this.options;
+ logger.debug(`Received event from ${params.topic}`);
+
+ const orgs = this.options.orgs?.length
+ ? this.options.orgs
+ : await this.getAllOrgs(this.options.gitHubConfig);
+
+ const eventPayload = params.eventPayload as
+ | InstallationEvent
+ | OrganizationEvent
+ | MembershipEvent
+ | TeamEvent;
+
+ if (
+ !orgs.includes(
+ (eventPayload as InstallationEvent).installation?.account?.login,
+ ) &&
+ !orgs.includes(
+ (eventPayload as OrganizationEvent | MembershipEvent | TeamEvent)
+ .organization?.login,
+ )
+ ) {
+ return;
+ }
+
+ // https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#installation
+ if (
+ params.topic.includes('installation') &&
+ eventPayload.action === 'created'
+ ) {
+ // We can only respond to installation.created events to add new users/groups since a
+ // installation.deleted event won't provide us info on what user/groups we should remove and
+ // we can't query the uninstalled org since we will no longer have access. This will need to be
+ // eventually resolved via occasional full mutation runs by calling read()
+ await this.onInstallationChange(
+ eventPayload as InstallationCreatedEvent,
+ orgs,
+ );
+ }
+
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#organization
+ if (
+ params.topic.includes('organization') &&
+ (eventPayload.action === 'member_added' ||
+ eventPayload.action === 'member_removed')
+ ) {
+ await this.onMemberChangeInOrganization(eventPayload, orgs);
+ }
+
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#team
+ if (params.topic.includes('team')) {
+ if (
+ eventPayload.action === 'created' ||
+ eventPayload.action === 'deleted'
+ ) {
+ await this.onTeamChangeInOrganization(
+ eventPayload as TeamCreatedEvent | TeamDeletedEvent,
+ );
+ } else if (eventPayload.action === 'edited') {
+ await this.onTeamEditedInOrganization(
+ eventPayload as TeamEditedEvent,
+ orgs,
+ );
+ }
+ }
+
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
+ if (params.topic.includes('membership')) {
+ this.onMembershipChangedInTeam(eventPayload as MembershipEvent, orgs);
+ }
+
+ return;
+ }
+
+ private async onInstallationChange(
+ event: InstallationCreatedEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const org = event.installation.account.login;
+ const { headers, type: tokenType } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const { users } = await getOrganizationUsers(
+ client,
+ org,
+ tokenType,
+ this.options.userTransformer,
+ );
+
+ const { groups } = await getOrganizationTeams(
+ client,
+ org,
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ // Fetch group memberships of users in case they already exist and
+ // have memberships in groups from other applicable orgs
+ for (const userOrg of applicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups: userGroups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ users.map(
+ u =>
+ u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] ||
+ u.metadata.name,
+ ),
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers(users, userGroups);
+ }
+
+ const { added, removed } = this.createAddEntitiesOperation([
+ ...users,
+ ...groups,
+ ]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onMemberChangeInOrganization(
+ event: OrganizationMemberAddedEvent | OrganizationMemberRemovedEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const userTransformer =
+ this.options.userTransformer || defaultUserTransformer;
+ const { name, avatar_url: avatarUrl, email, login } = event.membership.user;
+ const org = event.organization.login;
+ const { headers } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const { orgs } = await getOrganizationsFromUser(client, login);
+ const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o));
+
+ let updateMemberships: boolean;
+ let createDeltaOperation: CreateDeltaOperation;
+ if (event.action === 'member_removed') {
+ if (userApplicableOrgs.length) {
+ // If the user is still associated with another applicable org then we don't want to remove
+ // them, just update the entity to remove any potential group memberships from the old org
+ createDeltaOperation = this.createAddEntitiesOperation.bind(this);
+ updateMemberships = true;
+ } else {
+ // User is no longer part of any applicable orgs so we can remove it,
+ // no need to take memberships into account
+ createDeltaOperation = this.createRemoveEntitiesOperation.bind(this);
+ updateMemberships = false;
+ }
+ } else {
+ // We're not sure if this user was already added as part of another applicable org
+ // so grab the latest memberships (potentially from teams of other orgs) to ensure
+ // we're not accidentally omitting them
+ createDeltaOperation = this.createAddEntitiesOperation.bind(this);
+ updateMemberships = true;
+ }
+
+ const user = (await userTransformer(
+ {
+ name,
+ avatarUrl,
+ login,
+ email: email ?? undefined,
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as UserEntity;
+
+ if (updateMemberships) {
+ for (const userOrg of userApplicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ [login],
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers([user], groups);
+ }
+ }
+
+ const { added, removed } = createDeltaOperation([user]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onTeamChangeInOrganization(
+ event: TeamCreatedEvent | TeamDeletedEvent,
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const org = event.organization.login;
+ const { headers } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const { name, html_url: url, description, slug } = event.team;
+ const group = (await this.defaultMultiOrgTeamTransformer(
+ {
+ name,
+ slug,
+ editTeamUrl: `${url}/edit`,
+ combinedSlug: `${org}/${slug}`,
+ description: description ?? undefined,
+ parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam,
+ // entity will be removed or is new
+ members: [],
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as Entity;
+
+ const createDeltaOperation =
+ event.action === 'created'
+ ? this.createAddEntitiesOperation.bind(this)
+ : this.createRemoveEntitiesOperation.bind(this);
+ const { added, removed } = createDeltaOperation([group]);
+
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onTeamEditedInOrganization(
+ event: TeamEditedEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const org = event.organization.login;
+ const { headers, type: tokenType } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const teamSlug = event.team.slug;
+ const { group } = await getOrganizationTeam(
+ client,
+ org,
+ teamSlug,
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ const { users } = await getOrganizationUsers(
+ client,
+ org,
+ tokenType,
+ this.options.userTransformer,
+ );
+
+ const usersFromChangedGroup =
+ group.spec.members?.map(m =>
+ stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })),
+ ) || [];
+ const usersToRebuild = users.filter(u =>
+ usersFromChangedGroup.includes(stringifyEntityRef(u)),
+ );
+
+ // Update memberships of associated members of this group in case the group entity ref changed
+ for (const userOrg of applicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ usersToRebuild.map(
+ u =>
+ u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] ||
+ u.metadata.name,
+ ),
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers(usersToRebuild, groups);
+ }
+
+ const oldName = event.changes.name?.from || '';
+ const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
+ const oldGroup = (await this.defaultMultiOrgTeamTransformer(
+ {
+ name: event.changes.name?.from,
+ slug: oldSlug,
+ combinedSlug: `${org}/${oldSlug}`,
+ description: event.changes.description?.from,
+ parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam,
+ // entity will be removed
+ members: [],
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as Entity;
+
+ // Remove the old group entity in case the entity ref is now different
+ const { removed } = this.createRemoveEntitiesOperation([oldGroup]);
+ const { added } = this.createAddEntitiesOperation([
+ ...usersToRebuild,
+ group,
+ ]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onMembershipChangedInTeam(
+ event: MembershipEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ // The docs are saying I will receive the slug for the removed event,
+ // but the types don't reflect that,
+ // so I will just check to be sure the slug is there
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
+ if (!('slug' in event.team)) {
+ return;
+ }
+
+ const org = event.organization.login;
+ const { headers } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const teamSlug = event.team.slug;
+ const { group } = await getOrganizationTeam(
+ client,
+ org,
+ teamSlug,
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ const userTransformer =
+ this.options.userTransformer || defaultUserTransformer;
+ const { name, avatar_url: avatarUrl, email, login } = event.member;
+ const user = (await userTransformer(
+ {
+ name,
+ avatarUrl,
+ login,
+ email: email ?? undefined,
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as UserEntity;
+
+ const { orgs } = await getOrganizationsFromUser(client, login);
+ const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o));
+ for (const userOrg of userApplicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ [login],
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers([user], groups);
+ }
+
+ const { added, removed } = this.createAddEntitiesOperation([user, group]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
private schedule(schedule: GithubMultiOrgEntityProviderOptions['schedule']) {
if (!schedule || schedule === 'manual') {
return;
@@ -301,9 +810,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
team: GithubTeam,
ctx: TransformerContext,
): Promise {
- const result = this.options.teamTransformer
- ? await this.options.teamTransformer(team, ctx)
- : await defaultOrganizationTeamTransformer(team, ctx);
+ if (this.options.teamTransformer) {
+ return await this.options.teamTransformer(team, ctx);
+ }
+
+ const result = await defaultOrganizationTeamTransformer(team, ctx);
if (result) {
result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US');
@@ -333,6 +844,32 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
)
.filter(Boolean) as string[];
}
+
+ private createAddEntitiesOperation(entities: Entity[]) {
+ return {
+ removed: [],
+ added: entities.map(entity => ({
+ locationKey: `github-multi-org-provider:${this.options.id}`,
+ entity: withLocations(
+ `https://${this.options.gitHubConfig.host}`,
+ entity,
+ ),
+ })),
+ };
+ }
+
+ private createRemoveEntitiesOperation(entities: Entity[]) {
+ return {
+ added: [],
+ removed: entities.map(entity => ({
+ locationKey: `github-multi-org-provider:${this.options.id}`,
+ entity: withLocations(
+ `https://${this.options.gitHubConfig.host}`,
+ entity,
+ ),
+ })),
+ };
+ }
}
// Helps wrap the timing and logging behaviors
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts
index ff42e42718..3e58c1473f 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts
@@ -833,6 +833,7 @@ describe('GithubOrgEntityProvider', () => {
'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces',
},
name: 'mygroup-with-spaces',
+ description: 'description-from-the-new-team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts
index 02e225c938..c181510a67 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts
@@ -371,14 +371,21 @@ export class GithubOrgEntityProvider
assignGroupsToUsers(usersToRebuild, groups);
buildOrgHierarchy(groups);
- const oldName = event.changes.name?.from || '';
+ const oldName = event.changes.name?.from || event.team.name;
const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
+ const oldDescription =
+ event.changes.description?.from || event.team.description;
+ const oldDescriptionSlug = oldDescription
+ ?.toLowerCase()
+ .replaceAll(/\s/gi, '-');
+
const { removed } = createDeltaOperation(org, [
{
...group,
metadata: {
name: oldSlug,
+ description: oldDescriptionSlug,
},
},
]);
diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts
index 8f8c67895c..46932c95c6 100644
--- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts
+++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts
@@ -128,10 +128,11 @@ export class GitLabClient {
throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
}
memberIds.push(
- ...response.data.group.groupMembers.nodes.map(
- (node: { user: { id: string } }) =>
+ ...response.data.group.groupMembers.nodes
+ .filter(n => n.user)
+ .map(node =>
Number(node.user.id.replace(/^gid:\/\/gitlab\/User\//, '')),
- ),
+ ),
);
({ hasNextPage, endCursor } = response.data.group.groupMembers.pageInfo);
} while (hasNextPage);
diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
index 9921b0dabc..8c7b041d06 100644
--- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
+++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-backend-module-incremental-ingestion
+## 0.3.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
## 0.3.2-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json
index 686c89cd25..69bc2dfbd6 100644
--- a/plugins/catalog-backend-module-incremental-ingestion/package.json
+++ b/plugins/catalog-backend-module-incremental-ingestion/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-incremental-ingestion",
"description": "An entity provider for streaming large asset sources into the catalog",
- "version": "0.3.2-next.1",
+ "version": "0.3.2-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md
index 5fc587a4f9..345032bda7 100644
--- a/plugins/catalog-backend-module-openapi/CHANGELOG.md
+++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-catalog-backend-module-openapi
+## 0.1.11-next.2
+
+### Patch Changes
+
+- accaceadffa: Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
## 0.1.11-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json
index e02490ef59..c9960d15f1 100644
--- a/plugins/catalog-backend-module-openapi/package.json
+++ b/plugins/catalog-backend-module-openapi/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-openapi",
"description": "A Backstage catalog backend module that helps with OpenAPI specifications",
- "version": "0.1.11-next.1",
+ "version": "0.1.11-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts
index 64c772b91e..59f7e5791f 100644
--- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts
+++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts
@@ -65,6 +65,6 @@ export async function bundleFileWithRefs(
},
};
const fileObject = parse(fileWithRefs);
- const bundledObject = await $RefParser.bundle(fileObject, options);
+ const bundledObject = await $RefParser.bundle(baseUrl, fileObject, options);
return stringify(bundledObject);
}
diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
index 39bd1e3cdc..b5f13f39cd 100644
--- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
+++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-backend-module-puppetdb
+## 0.1.2-next.2
+
+### Patch Changes
+
+- 95b2168d71b: Fixes import paths and updates documentation
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.1.2-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-puppetdb/README.md b/plugins/catalog-backend-module-puppetdb/README.md
index d4782c6d18..9319299a06 100644
--- a/plugins/catalog-backend-module-puppetdb/README.md
+++ b/plugins/catalog-backend-module-puppetdb/README.md
@@ -31,9 +31,9 @@ Update the catalog plugin initialization in your backend to add the provider and
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 10 },
+ timeout: { minutes: 50 },
-+ initialDelay: { seconds: 15}
++ initialDelay: { seconds: 15 }
+ }),
-+ });
++ })
+ );
```
@@ -67,7 +67,7 @@ of overriding the default transformer.
export const customResourceTransformer: ResourceTransformer = async (
node,
config,
-): Promise => {
+): Promise => {
// Transformations may change namespace, owner, change entity naming pattern, add labels, annotations, etc.
// Create the Resource Entity on your own, or wrap the default transformer
@@ -77,14 +77,16 @@ export const customResourceTransformer: ResourceTransformer = async (
2. Configure the provider with the transformer:
-```ts
-const puppetDbEntityProvider = PuppetDbEntityProvider.fromConfig(env.config, {
- logger: env.logger,
- schedule: env.scheduler.createScheduledTaskRunner({
- frequency: { minutes: 10 },
- timeout: { minutes: 50 },
- initialDelay: { seconds: 15 },
- }),
- transformer: customResourceTransformer,
-});
+```diff
+ builder.addEntityProvider(
+ PuppetDbEntityProvider.fromConfig(env.config, {
+ logger: env.logger,
++ transformer: customResourceTransformer,
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 10 },
+ timeout: { minutes: 50 },
+ initialDelay: { seconds: 15 }
+ }),
+ })
+ );
```
diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json
index 65bda327dd..3d1734657b 100644
--- a/plugins/catalog-backend-module-puppetdb/package.json
+++ b/plugins/catalog-backend-module-puppetdb/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-puppetdb",
"description": "A Backstage catalog backend module that helps integrate towards PuppetDB",
- "version": "0.1.2-next.1",
+ "version": "0.1.2-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts
index ad89e1fd9a..42091fd6d6 100644
--- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts
@@ -28,7 +28,7 @@ import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
ResourceEntity,
-} from '@backstage/catalog-model/';
+} from '@backstage/catalog-model';
import { DEFAULT_ENTITY_OWNER, ENDPOINT_NODES } from '../puppet/constants';
jest.mock('../puppet/read', () => {
diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts
index 8c63baf467..a5286a9d70 100644
--- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts
+++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts
@@ -31,7 +31,7 @@ import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
Entity,
-} from '@backstage/catalog-model/';
+} from '@backstage/catalog-model';
import { merge } from 'lodash';
import { readPuppetNodes } from '../puppet/read';
import { ENDPOINT_NODES } from '../puppet/constants';
diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts
index 568ea7224e..ab3e85370a 100644
--- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts
+++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts
@@ -16,7 +16,7 @@
import { PuppetDbEntityProviderConfig } from '../providers';
import { PuppetNode, ResourceTransformer } from './types';
-import { ResourceEntity } from '@backstage/catalog-model/';
+import { ResourceEntity } from '@backstage/catalog-model';
import { defaultResourceTransformer } from './transformers';
import fetch from 'node-fetch';
import { ResponseError } from '@backstage/errors';
diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md
index 1a715e26bc..78021149ff 100644
--- a/plugins/catalog-backend/CHANGELOG.md
+++ b/plugins/catalog-backend/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-catalog-backend
+## 1.9.1-next.2
+
+### Patch Changes
+
+- ce8d203235b: Ensure that entity cache state is only written to the database when actually changed
+- 485a6c5f7b5: Internal refactoring for performance in the service handlers
+- 3587a968dcd: Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests.
+- 12a345317ab: Remove unnecessary join in the entity facets endpoint, exclude nulls
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+ - @backstage/config@1.0.7
+
## 1.9.1-next.1
### Patch Changes
diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json
index 289a9a801d..13f358f1ff 100644
--- a/plugins/catalog-backend/package.json
+++ b/plugins/catalog-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend",
"description": "The Backstage backend plugin that provides the Backstage catalog",
- "version": "1.9.1-next.1",
+ "version": "1.9.1-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts
index 920acae7bd..eeab85f59e 100644
--- a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts
+++ b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts
@@ -33,13 +33,16 @@ export async function deleteOrphanedEntities(options: {
// Limit iterations for sanity
for (let i = 0; i < 100; ++i) {
const candidates = await tx
- .with('orphans', orphans =>
+ .with('orphans', ['entity_id', 'entity_ref'], orphans =>
orphans
- .from('refresh_state')
- .select('entity_id', 'entity_ref')
- .whereNotIn('entity_ref', keep =>
- keep.distinct('target_entity_ref').from('refresh_state_references'),
- ),
+ .from('refresh_state')
+ .select('refresh_state.entity_id', 'refresh_state.entity_ref')
+ .leftOuterJoin(
+ 'refresh_state_references',
+ 'refresh_state_references.target_entity_ref',
+ 'refresh_state.entity_ref',
+ )
+ .whereNull('refresh_state_references.target_entity_ref'),
)
.select({
entityId: 'orphans.entity_id',
diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts
index ab1e0b96c9..8e2f8b2f6f 100644
--- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts
+++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts
@@ -150,7 +150,10 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
track.markProcessorsCompleted(result);
if (result.ok) {
- if (stableStringify(state) !== stableStringify(result.state)) {
+ const { ttl: _, ...stateWithoutTtl } = state ?? {};
+ if (
+ stableStringify(stateWithoutTtl) !== stableStringify(result.state)
+ ) {
await this.processingDatabase.transaction(async tx => {
await this.processingDatabase.updateEntityCache(tx, {
id,
@@ -314,7 +317,9 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
try {
await this.processingDatabase.transaction(async tx => {
const n = await this.processingDatabase.deleteOrphanedEntities(tx);
- this.logger.info(`Deleted ${n} orphaned entities`);
+ if (n > 0) {
+ this.logger.info(`Deleted ${n} orphaned entities`);
+ }
});
} catch (error) {
this.logger.warn(`Failed to delete orphaned entities`, error);
diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
index 08ae295834..4f011813e6 100644
--- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
+++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
@@ -1418,6 +1418,87 @@ describe('DefaultEntitiesCatalog', () => {
},
);
+ it.each(databases.eachSupportedId())(
+ 'should exclude filtered entities when paginating, %p',
+ async databaseId => {
+ await createDatabase(databaseId);
+
+ await Promise.all([
+ addEntityToSearch(entityFrom('AA', { uid: '1', kind: 'included' })),
+ addEntityToSearch(
+ entityFrom('AA', {
+ namespace: 'namespace2',
+ kind: 'included',
+ uid: '2',
+ }),
+ ),
+ addEntityToSearch(
+ entityFrom('AA', {
+ namespace: 'ns',
+ kind: 'excluded',
+ uid: '3',
+ }),
+ ),
+ addEntityToSearch(
+ entityFrom('AA', {
+ namespace: 'namespace3',
+ uid: '4',
+ kind: 'included',
+ }),
+ ),
+ addEntityToSearch(
+ entityFrom('AA', {
+ namespace: 'namespace4',
+ uid: '5',
+ kind: 'included',
+ }),
+ ),
+ addEntityToSearch(entityFrom('CC', { uid: '6', kind: 'included' })),
+ addEntityToSearch(entityFrom('DD', { uid: '7', kind: 'included' })),
+ ]);
+
+ const catalog = new DefaultEntitiesCatalog({
+ database: knex,
+ logger: getVoidLogger(),
+ stitcher,
+ });
+
+ const limit = 2;
+
+ // initial request
+ const request1: QueryEntitiesInitialRequest = {
+ limit,
+ filter: {
+ key: 'kind',
+ values: ['included'],
+ },
+ orderFields: [{ field: 'metadata.name', order: 'asc' }],
+ };
+ const response1 = await catalog.queryEntities(request1);
+ expect(response1.items).toMatchObject([
+ entityFrom('AA', { uid: '1', kind: 'included' }),
+ entityFrom('AA', { uid: '2', kind: 'included' }),
+ ]);
+ expect(response1.pageInfo.nextCursor).toBeDefined();
+ expect(response1.pageInfo.prevCursor).toBeUndefined();
+ expect(response1.totalItems).toBe(6);
+
+ // second request (forward)
+ const request2: QueryEntitiesCursorRequest = {
+ cursor: response1.pageInfo.nextCursor!,
+ limit,
+ };
+ const response2 = await catalog.queryEntities(request2);
+ expect(response2.items).toMatchObject([
+ entityFrom('AA', { uid: '4', kind: 'included' }),
+ entityFrom('AA', { uid: '5', kind: 'included' }),
+ ]);
+ expect(response2.pageInfo.nextCursor).toBeDefined();
+ expect(response2.pageInfo.prevCursor).toBeDefined();
+ expect(response2.totalItems).toBe(6);
+ },
+ );
+
it.each(databases.eachSupportedId())(
'should paginate results without sort fields, %p',
async databaseId => {
@@ -1648,6 +1729,17 @@ describe('DefaultEntitiesCatalog', () => {
],
},
});
+
+ await expect(
+ catalog.facets({
+ facets: ['kind'],
+ filter: { not: { key: 'metadata.name', values: ['two'] } },
+ }),
+ ).resolves.toEqual({
+ facets: {
+ kind: [{ value: 'k', count: 1 }],
+ },
+ });
},
);
@@ -1754,11 +1846,12 @@ function entityFrom(
uid,
namespace,
title,
- }: { uid?: string; namespace?: string; title?: string } = {},
+ kind = 'k',
+ }: { uid?: string; namespace?: string; title?: string; kind?: string } = {},
) {
return {
apiVersion: 'a',
- kind: 'k',
+ kind,
metadata: {
name,
...(!!namespace && { namespace }),
diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
index ba165f96af..c328c83823 100644
--- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
+++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
@@ -64,43 +64,47 @@ const defaultSortField: EntityOrder = {
const DEFAULT_LIMIT = 20;
-function parsePagination(input?: EntityPagination): {
- limit?: number;
- offset?: number;
-} {
+function parsePagination(input?: EntityPagination): EntityPagination {
if (!input) {
return {};
}
let { limit, offset } = input;
- if (input.after !== undefined) {
- let cursor;
- try {
- const json = Buffer.from(input.after, 'base64').toString('utf8');
- cursor = JSON.parse(json);
- } catch {
- throw new InputError('Malformed after cursor, could not be parsed');
+ if (input.after === undefined) {
+ return { limit, offset };
+ }
+
+ let cursor;
+ try {
+ const json = Buffer.from(input.after, 'base64').toString('utf8');
+ cursor = JSON.parse(json);
+ } catch {
+ throw new InputError('Malformed after cursor, could not be parsed');
+ }
+
+ if (cursor.limit !== undefined) {
+ if (!Number.isInteger(cursor.limit)) {
+ throw new InputError('Malformed after cursor, limit was not an number');
}
- if (cursor.limit !== undefined) {
- if (!Number.isInteger(cursor.limit)) {
- throw new InputError('Malformed after cursor, limit was not an number');
- }
- limit = cursor.limit;
- }
- if (cursor.offset !== undefined) {
- if (!Number.isInteger(cursor.offset)) {
- throw new InputError('Malformed after cursor, offset was not a number');
- }
- offset = cursor.offset;
+ limit = cursor.limit;
+ }
+
+ if (cursor.offset !== undefined) {
+ if (!Number.isInteger(cursor.offset)) {
+ throw new InputError('Malformed after cursor, offset was not a number');
}
+ offset = cursor.offset;
}
return { limit, offset };
}
-function stringifyPagination(input: { limit: number; offset: number }) {
- const json = JSON.stringify({ limit: input.limit, offset: input.offset });
+function stringifyPagination(
+ input: Required>,
+): string {
+ const { limit, offset } = input;
+ const json = JSON.stringify({ limit, offset });
const base64 = Buffer.from(json, 'utf8').toString('base64');
return base64;
}
@@ -111,24 +115,21 @@ function addCondition(
filter: EntitiesSearchFilter,
negate: boolean = false,
entityIdField = 'entity_id',
-) {
+): void {
+ const key = filter.key.toLowerCase();
+ const values = filter.values?.map(v => v.toLowerCase());
+
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
// make a lot of sense. However, it had abysmal performance on sqlite
// when datasets grew large, so we're using IN instead.
const matchQuery = db('search')
.select('search.entity_id')
- .where({ key: filter.key.toLowerCase() })
+ .where({ key })
.andWhere(function keyFilter() {
- if (filter.values) {
- if (filter.values.length === 1) {
- this.where({ value: filter.values[0].toLowerCase() });
- } else {
- this.andWhere(
- 'value',
- 'in',
- filter.values.map(v => v.toLowerCase()),
- );
- }
+ if (values?.length === 1) {
+ this.where({ value: values.at(0) });
+ } else if (values) {
+ this.andWhere('value', 'in', values);
}
});
queryBuilder.andWhere(entityIdField, negate ? 'not in' : 'in', matchQuery);
@@ -159,16 +160,16 @@ function parseFilter(
negate: boolean = false,
entityIdField = 'entity_id',
): Knex.QueryBuilder {
+ if (isNegationEntityFilter(filter)) {
+ return parseFilter(filter.not, query, db, !negate, entityIdField);
+ }
+
if (isEntitiesSearchFilter(filter)) {
return query.andWhere(function filterFunction() {
addCondition(this, db, filter, negate, entityIdField);
});
}
- if (isNegationEntityFilter(filter)) {
- return parseFilter(filter.not, query, db, !negate, entityIdField);
- }
-
return query[negate ? 'andWhereNot' : 'andWhere'](function filterFunction() {
if (isOrEntityFilter(filter)) {
for (const subFilter of filter.anyOf ?? []) {
@@ -413,17 +414,18 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
const isOrderingDescending = sortField.order === 'desc';
if (prevItemOrderFieldValue) {
- dbQuery.andWhere(
- 'value',
- isFetchingBackwards !== isOrderingDescending ? '<' : '>',
- prevItemOrderFieldValue,
- );
- dbQuery.orWhere(function nested() {
- this.where('value', '=', prevItemOrderFieldValue).andWhere(
- 'search.entity_id',
+ dbQuery.andWhere(function nested() {
+ this.where(
+ 'value',
isFetchingBackwards !== isOrderingDescending ? '<' : '>',
- prevItemUid,
- );
+ prevItemOrderFieldValue,
+ )
+ .orWhere('value', '=', prevItemOrderFieldValue)
+ .andWhere(
+ 'search.entity_id',
+ isFetchingBackwards !== isOrderingDescending ? '<' : '>',
+ prevItemUid,
+ );
});
}
@@ -677,10 +679,9 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
for (const facet of request.facets) {
const dbQuery = db('search')
- .join('final_entities', 'search.entity_id', 'final_entities.entity_id')
.where('search.key', facet.toLocaleLowerCase('en-US'))
- .count('search.entity_id as count')
- .select({ value: 'search.original_value' })
+ .whereNotNull('search.original_value')
+ .select({ value: 'search.original_value', count: db.raw('count(*)') })
.groupBy('search.original_value');
if (request?.filter) {
diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md
index 138edaee90..0f8d9dccb1 100644
--- a/plugins/catalog-customized/CHANGELOG.md
+++ b/plugins/catalog-customized/CHANGELOG.md
@@ -1,5 +1,13 @@
# @internal/plugin-catalog-customized
+## 0.0.10-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+
## 0.0.10-next.1
### Patch Changes
diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json
index 808f7a40e4..f7a6b0b80f 100644
--- a/plugins/catalog-customized/package.json
+++ b/plugins/catalog-customized/package.json
@@ -1,7 +1,7 @@
{
"name": "@internal/plugin-catalog-customized",
"description": "The internal Backstage Customizable plugin for browsing the Backstage catalog",
- "version": "0.0.10-next.1",
+ "version": "0.0.10-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md
index eabfdaff12..00c907899a 100644
--- a/plugins/catalog-graph/CHANGELOG.md
+++ b/plugins/catalog-graph/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-catalog-graph
+## 0.2.30-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.30-next.1
### Patch Changes
diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json
index b9ac5ea1c1..bd9508783d 100644
--- a/plugins/catalog-graph/package.json
+++ b/plugins/catalog-graph/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-graph",
- "version": "0.2.30-next.1",
+ "version": "0.2.30-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx
index 85d9d97c7f..242bed6311 100644
--- a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx
+++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { render, waitFor, screen } from '@testing-library/react';
+import { render, waitFor, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CurveFilter } from './CurveFilter';
@@ -33,7 +33,9 @@ describe('', () => {
expect(screen.getByText('Step Before')).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('select'));
+ await userEvent.click(
+ within(screen.getByTestId('select')).getByRole('button'),
+ );
await userEvent.click(screen.getByText('Monotone X'));
await waitFor(() => {
diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx
index cb660b5a25..dc74218316 100644
--- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx
+++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { render, waitFor, screen } from '@testing-library/react';
+import { render, waitFor, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Direction } from '../EntityRelationsGraph';
@@ -37,7 +37,9 @@ describe('', () => {
expect(screen.getByText('Right to left')).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('select'));
+ await userEvent.click(
+ within(screen.getByTestId('select')).getByRole('button'),
+ );
await userEvent.click(screen.getByText('Top to bottom'));
await waitFor(() => {
diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md
index 738a28f3af..38ae6fbed8 100644
--- a/plugins/catalog-import/CHANGELOG.md
+++ b/plugins/catalog-import/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-catalog-import
+## 0.9.8-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.9.8-next.1
### Patch Changes
diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json
index 7e7113a16c..5fb8bb1b5b 100644
--- a/plugins/catalog-import/package.json
+++ b/plugins/catalog-import/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-import",
"description": "A Backstage plugin the helps you import entities into your catalog",
- "version": "0.9.8-next.1",
+ "version": "0.9.8-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md
index bd9d13ace5..79ade18d8f 100644
--- a/plugins/catalog-react/CHANGELOG.md
+++ b/plugins/catalog-react/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-catalog-react
+## 1.6.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
## 1.6.0-next.1
### Patch Changes
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index 37190aa4c8..d5808553cb 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-react",
"description": "A frontend library that helps other Backstage plugins interact with the catalog",
- "version": "1.6.0-next.1",
+ "version": "1.6.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
index 04d8df3d16..2513f8dca9 100644
--- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
@@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model';
import { ApiProvider } from '@backstage/core-app-api';
import { alertApiRef } from '@backstage/core-plugin-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
-import { fireEvent, waitFor, screen } from '@testing-library/react';
+import { fireEvent, waitFor, screen, within } from '@testing-library/react';
import { capitalize } from 'lodash';
import { default as React } from 'react';
import { catalogApiRef } from '../../api';
@@ -87,7 +87,7 @@ describe('', () => {
expect(screen.getByText('Kind')).toBeInTheDocument();
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('Domain'));
@@ -115,7 +115,7 @@ describe('', () => {
,
);
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('Domain'));
fireEvent.click(screen.getByText('Domain'));
@@ -171,7 +171,7 @@ describe('', () => {
);
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
expect(
screen.getByRole('option', { name: 'Component' }),
@@ -196,7 +196,7 @@ describe('', () => {
expect(screen.getByText('Frob')).toBeInTheDocument();
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
expect(screen.getByRole('option', { name: 'Domain' })).toBeInTheDocument();
});
});
diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx
index d61a6c480f..3e69c35180 100644
--- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx
@@ -15,7 +15,7 @@
*/
import React from 'react';
-import { fireEvent, waitFor, screen } from '@testing-library/react';
+import { fireEvent, waitFor, screen, within } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { EntityTypePicker } from './EntityTypePicker';
import { MockEntityListContextProvider } from '../../testUtils/providers';
@@ -95,7 +95,7 @@ describe('', () => {
expect(screen.getByText('Type')).toBeInTheDocument();
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('service'));
@@ -119,7 +119,7 @@ describe('', () => {
,
);
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('service'));
fireEvent.click(screen.getByText('service'));
@@ -128,7 +128,7 @@ describe('', () => {
type: new EntityTypeFilter(['service']),
});
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
fireEvent.click(screen.getByText('all'));
expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined });
diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md
index 1082396da1..8300e81422 100644
--- a/plugins/catalog/CHANGELOG.md
+++ b/plugins/catalog/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-catalog
+## 1.11.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 1.11.0-next.1
### Patch Changes
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 0d5a9badcd..2a0cfb4625 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog",
"description": "The Backstage plugin for browsing the Backstage catalog",
- "version": "1.11.0-next.1",
+ "version": "1.11.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
index d895633ad4..93b77f90f8 100644
--- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
+++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-cicd-statistics-module-gitlab
+## 0.1.14-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-cicd-statistics@0.1.20-next.2
+
## 0.1.14-next.1
### Patch Changes
diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json
index 20d82e485f..37bf35a907 100644
--- a/plugins/cicd-statistics-module-gitlab/package.json
+++ b/plugins/cicd-statistics-module-gitlab/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cicd-statistics-module-gitlab",
"description": "CI/CD Statistics plugin module; Gitlab CICD",
- "version": "0.1.14-next.1",
+ "version": "0.1.14-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md
index 3a802df672..a321674f8d 100644
--- a/plugins/cicd-statistics/CHANGELOG.md
+++ b/plugins/cicd-statistics/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-cicd-statistics
+## 0.1.20-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.20-next.1
### Patch Changes
diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json
index a6a01ae05c..dce541c0b8 100644
--- a/plugins/cicd-statistics/package.json
+++ b/plugins/cicd-statistics/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cicd-statistics",
"description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)",
- "version": "0.1.20-next.1",
+ "version": "0.1.20-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md
index 8afa59660d..afa6a83d48 100644
--- a/plugins/circleci/CHANGELOG.md
+++ b/plugins/circleci/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-circleci
+## 0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.18-next.1
### Patch Changes
diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json
index 683b43795c..4108700972 100644
--- a/plugins/circleci/package.json
+++ b/plugins/circleci/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-circleci",
"description": "A Backstage plugin that integrates towards Circle CI",
- "version": "0.3.18-next.1",
+ "version": "0.3.18-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md
index ba462e356c..048c6b73ca 100644
--- a/plugins/cloudbuild/CHANGELOG.md
+++ b/plugins/cloudbuild/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-cloudbuild
+## 0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.18-next.1
### Patch Changes
diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json
index 26a72a6c84..ef57f77fde 100644
--- a/plugins/cloudbuild/package.json
+++ b/plugins/cloudbuild/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cloudbuild",
"description": "A Backstage plugin that integrates towards Google Cloud Build",
- "version": "0.3.18-next.1",
+ "version": "0.3.18-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md
index 405e65e814..c31c3ca3f3 100644
--- a/plugins/code-climate/CHANGELOG.md
+++ b/plugins/code-climate/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-code-climate
+## 0.1.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.18-next.1
### Patch Changes
diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json
index 2edda59078..f650739510 100644
--- a/plugins/code-climate/package.json
+++ b/plugins/code-climate/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-code-climate",
- "version": "0.1.18-next.1",
+ "version": "0.1.18-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md
index 7f5789d3a5..e1ac2d6b77 100644
--- a/plugins/code-coverage/CHANGELOG.md
+++ b/plugins/code-coverage/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-code-coverage
+## 0.2.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.11-next.1
### Patch Changes
diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json
index 412c7d3281..7e40175240 100644
--- a/plugins/code-coverage/package.json
+++ b/plugins/code-coverage/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-code-coverage",
"description": "A Backstage plugin that helps you keep track of your code coverage",
- "version": "0.2.11-next.1",
+ "version": "0.2.11-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md
index 3c6338dfb0..4483bc8351 100644
--- a/plugins/codescene/CHANGELOG.md
+++ b/plugins/codescene/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-codescene
+## 0.1.13-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.13-next.0
### Patch Changes
diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json
index 744634130c..05f2d8cde1 100644
--- a/plugins/codescene/package.json
+++ b/plugins/codescene/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-codescene",
- "version": "0.1.13-next.0",
+ "version": "0.1.13-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md
index e744ff73b1..71c47d650c 100644
--- a/plugins/config-schema/CHANGELOG.md
+++ b/plugins/config-schema/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-config-schema
+## 0.1.41-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.41-next.0
### Patch Changes
diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json
index 893048274e..bf8bfc2d9d 100644
--- a/plugins/config-schema/package.json
+++ b/plugins/config-schema/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-config-schema",
"description": "A Backstage plugin that lets you browse the configuration schema of your app",
- "version": "0.1.41-next.0",
+ "version": "0.1.41-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md
index 89ba2ebaa0..b9a289e963 100644
--- a/plugins/cost-insights/CHANGELOG.md
+++ b/plugins/cost-insights/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-cost-insights
+## 0.12.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.12.7-next.1
### Patch Changes
diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md
index 1e3f4e9de6..4fbf8a0cbe 100644
--- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md
+++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md
@@ -75,7 +75,7 @@ try {
We can use the data provided by the Cost Explorer API to implement CostInsightsApi methods such as `getGroupDailyCost`, `getProjectDailyCost`, and `getProductInsights`. You'll still need to provide external data for methods such as `getUserGroups` and `getAlerts`.
-### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L93)
+### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L119)
The `getGroupDailyCost` method is expected to return daily cost aggregations for a given group and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Cost.ts).
@@ -206,13 +206,13 @@ Sample `groupedCosts` based on the response:
}
```
-### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111)
+### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L137)
The `getProjectDailyCost` method is expected to return daily cost aggregations for a given billing entity and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Cost.ts).
This should be similar to the `getGroupDailyCost` method implementation, but with an updated `LINKED_ACCOUNT` filter to get narrower cost data for a lower-level linked account.
-### 3. [getProductInsights](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111)
+### 3. [getProductInsights](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L165)
The `getProductInsights` method is expected to return cost aggregations for a particular cloud product and interval time frame as an [`Entity`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Entity.ts).
diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json
index 4855aae01a..62295bf737 100644
--- a/plugins/cost-insights/package.json
+++ b/plugins/cost-insights/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cost-insights",
"description": "A Backstage plugin that helps you keep track of your cloud spend",
- "version": "0.12.7-next.1",
+ "version": "0.12.7-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/devtools-backend/.eslintrc.js b/plugins/devtools-backend/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/devtools-backend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md
new file mode 100644
index 0000000000..6fefbd1524
--- /dev/null
+++ b/plugins/devtools-backend/CHANGELOG.md
@@ -0,0 +1,13 @@
+# @backstage/plugin-devtools-backend
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0-next.0
+ - @backstage/config@1.0.7
diff --git a/plugins/devtools-backend/README.md b/plugins/devtools-backend/README.md
new file mode 100644
index 0000000000..e505a04963
--- /dev/null
+++ b/plugins/devtools-backend/README.md
@@ -0,0 +1,56 @@
+# DevTools Backend
+
+Welcome to the DevTools backend plugin! This plugin provides data for the [DevTools frontend](../devtools/) features.
+
+## Setup
+
+Here's how to get the DevTools Backend up and running:
+
+1. First we need to add the `@backstage/plugin-devtools-backend` package to your backend:
+
+ ```sh
+ # From the Backstage root directory
+ cd packages/backend
+ yarn add @backstage/plugin-devtools-backend
+ ```
+
+2. Then we will create a new file named `packages/backend/src/plugins/devtools.ts`, and add the
+ following to it:
+
+ ```ts
+ import { createRouter } from '@backstage/plugin-devtools-backend';
+ import { Router } from 'express';
+ import type { PluginEnvironment } from '../types';
+
+ export default function createPlugin(
+ env: PluginEnvironment,
+ ): Promise {
+ return createRouter({
+ logger: env.logger,
+ config: env.config,
+ permissions: env.permissions,
+ });
+ }
+ ```
+
+3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
+
+ ```ts
+ import devTools from './plugins/devtools';
+ // ...
+ async function main() {
+ // ...
+ // Add this line under the other lines that follow the useHotMemoize pattern
+ const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
+ // ...
+ // Insert this line under the other lines that add their routers to apiRouter in the same way
+ apiRouter.use('/devtools', await devTools(devToolsEnv));
+ ```
+
+4. Now run `yarn start-backend` from the repo root
+5. Finally open `http://localhost:7007/api/devtools/health` in a browser and it should return `{"status":"ok"}`
+
+## Links
+
+- [Frontend part of the plugin](../devtools/README.md)
+- [The Backstage homepage](https://backstage.io)
diff --git a/plugins/devtools-backend/api-report.md b/plugins/devtools-backend/api-report.md
new file mode 100644
index 0000000000..e28f0697ab
--- /dev/null
+++ b/plugins/devtools-backend/api-report.md
@@ -0,0 +1,39 @@
+## API Report File for "@backstage/plugin-devtools-backend"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { Config } from '@backstage/config';
+import { ConfigInfo } from '@backstage/plugin-devtools-common';
+import { DevToolsInfo } from '@backstage/plugin-devtools-common';
+import express from 'express';
+import { ExternalDependency } from '@backstage/plugin-devtools-common';
+import { Logger } from 'winston';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+
+// @public (undocumented)
+export function createRouter(options: RouterOptions): Promise;
+
+// @public (undocumented)
+export class DevToolsBackendApi {
+ constructor(logger: Logger, config: Config);
+ // (undocumented)
+ listConfig(): Promise;
+ // (undocumented)
+ listExternalDependencyDetails(): Promise;
+ // (undocumented)
+ listInfo(): Promise;
+}
+
+// @public (undocumented)
+export interface RouterOptions {
+ // (undocumented)
+ config: Config;
+ // (undocumented)
+ devToolsBackendApi?: DevToolsBackendApi;
+ // (undocumented)
+ logger: Logger;
+ // (undocumented)
+ permissions: PermissionEvaluator;
+}
+```
diff --git a/plugins/devtools-backend/config.d.ts b/plugins/devtools-backend/config.d.ts
new file mode 100644
index 0000000000..15e70afcf8
--- /dev/null
+++ b/plugins/devtools-backend/config.d.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export interface Config {
+ /**
+ * DevTools configuration.
+ */
+ devTools?: {
+ /**
+ * External dependency configuration.
+ */
+ externalDependencies?: {
+ /**
+ * The list of endpoints to check.
+ */
+ endpoints?: Array<{
+ /**
+ * The name of the endpoint.
+ */
+ name: string;
+ /**
+ * Type of check to perform; currently fetch or ping
+ */
+ type: string;
+ /**
+ * The target of the endpoint; currently either a URL for fetch or server name for ping.
+ */
+ target: string;
+ }>;
+ };
+ };
+}
diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json
new file mode 100644
index 0000000000..7235f1293a
--- /dev/null
+++ b/plugins/devtools-backend/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@backstage/plugin-devtools-backend",
+ "version": "0.1.0-next.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin"
+ },
+ "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/backend-common": "workspace:^",
+ "@backstage/cli-common": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/config-loader": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "@backstage/plugin-devtools-common": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/plugin-permission-node": "workspace:^",
+ "@backstage/types": "workspace:^",
+ "@manypkg/get-packages": "^1.1.3",
+ "@types/express": "*",
+ "@yarnpkg/lockfile": "^1.1.0",
+ "@yarnpkg/parsers": "^3.0.0-rc.4",
+ "express": "^4.18.1",
+ "express-promise-router": "^4.1.0",
+ "fs-extra": "^10.0.0",
+ "lodash": "^4.17.21",
+ "node-fetch": "^2.6.7",
+ "ping": "^0.4.1",
+ "semver": "^7.3.2",
+ "winston": "^3.2.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@types/minimist": "^1.2.0",
+ "@types/ping": "^0.4.1",
+ "@types/supertest": "^2.0.8",
+ "@types/yarnpkg__lockfile": "^1.1.4",
+ "msw": "^0.47.0",
+ "supertest": "^6.2.4"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/devtools-backend/src/api/DevToolsBackendApi.ts b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts
new file mode 100644
index 0000000000..efd0407b31
--- /dev/null
+++ b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config, ConfigReader } from '@backstage/config';
+import { loadConfigSchema } from '@backstage/config-loader';
+import {
+ PackageDependency,
+ DevToolsInfo,
+ ExternalDependency,
+ Endpoint,
+ ExternalDependencyStatus,
+ ConfigInfo,
+} from '@backstage/plugin-devtools-common';
+
+import { JsonObject } from '@backstage/types';
+import { Logger } from 'winston';
+import fetch from 'node-fetch';
+import { findPaths } from '@backstage/cli-common';
+import { getPackages } from '@manypkg/get-packages';
+import ping from 'ping';
+import os from 'os';
+import fs from 'fs-extra';
+import { Lockfile } from '../util/Lockfile';
+import { memoize } from 'lodash';
+import { assertError } from '@backstage/errors';
+
+/** @public */
+export class DevToolsBackendApi {
+ public constructor(
+ private readonly logger: Logger,
+ private readonly config: Config,
+ ) {}
+
+ public async listExternalDependencyDetails(): Promise {
+ const result: ExternalDependency[] = [];
+
+ const endpoints = this.config.getOptional(
+ 'devTools.externalDependencies.endpoints',
+ );
+ if (!endpoints) {
+ // No external dependency endpoints configured
+ return result;
+ }
+ for (const endpoint of endpoints) {
+ this.logger?.info(
+ `Checking external dependency "${endpoint.name}" at "${endpoint.target}"`,
+ );
+
+ switch (endpoint.type) {
+ case 'ping': {
+ const pingResult = await this.pingExternalDependency(endpoint);
+ result.push(pingResult);
+ break;
+ }
+ case 'fetch': {
+ const fetchResult = await this.fetchExternalDependency(endpoint);
+ result.push(fetchResult);
+ break;
+ }
+ default:
+ return result;
+ }
+ }
+
+ return result;
+ }
+
+ private async fetchExternalDependency(
+ endpoint: Endpoint,
+ ): Promise {
+ let status;
+ let error;
+
+ await fetch(endpoint.target)
+ .then(res => {
+ status =
+ res.status === 200
+ ? ExternalDependencyStatus.healthy
+ : ExternalDependencyStatus.unhealthy;
+ this.logger.debug(
+ `Fetch for ${endpoint.name} resulted in status code "${res.status}"`,
+ );
+ })
+ .catch((err: Error) => {
+ this.logger.error(`Fetch failed for ${endpoint.name} - ${err.message}`);
+ error = err.message;
+ });
+
+ const result: ExternalDependency = {
+ name: endpoint.name,
+ type: endpoint.type,
+ target: endpoint.target,
+ status: status ?? ExternalDependencyStatus.unhealthy,
+ error: error ?? undefined,
+ };
+
+ return result;
+ }
+
+ private async pingExternalDependency(
+ endpoint: Endpoint,
+ ): Promise {
+ const pingResult = await ping.promise.probe(endpoint.target);
+
+ let error;
+ if (
+ pingResult.packetLoss === '100.000' ||
+ pingResult.packetLoss === 'unknown'
+ ) {
+ this.logger.error(
+ `Ping failed for ${endpoint.name} - ${pingResult.output}`,
+ );
+ error =
+ pingResult.output === ''
+ ? `${endpoint.target} - Unknown`
+ : pingResult.output;
+ }
+
+ this.logger.debug(
+ `Ping results for ${endpoint.name}: ${pingResult.output}`,
+ );
+
+ const result: ExternalDependency = {
+ name: endpoint.name,
+ type: endpoint.type,
+ target: endpoint.target,
+ status: pingResult.alive
+ ? ExternalDependencyStatus.healthy
+ : ExternalDependencyStatus.unhealthy,
+ error: error ?? undefined,
+ };
+
+ return result;
+ }
+
+ public async listConfig(): Promise {
+ /* eslint-disable-next-line no-restricted-syntax */
+ const paths = findPaths(__dirname);
+
+ const { packages } = await getPackages(paths.targetDir);
+ const schemaFunc = async () => {
+ return await loadConfigSchema({
+ dependencies: packages.map(p => p.packageJson.name),
+ });
+ };
+
+ const schemaMemo = memoize(schemaFunc);
+ const schema = await schemaMemo();
+
+ const configInfo: ConfigInfo = {
+ config: undefined,
+ error: undefined,
+ };
+ try {
+ const config = {
+ data: this.config.get() as JsonObject,
+ context: 'inline',
+ };
+ const sanitizedConfigs = schema.process([config], {
+ ignoreSchemaErrors: false,
+ valueTransform: (value, context) =>
+ context.visibility === 'secret' ? '' : value,
+ });
+
+ const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
+ configInfo.config = data;
+ } catch (error) {
+ assertError(error);
+ // The config is not valid for some reason but we want to be able to see it still
+ const config = {
+ data: this.config.get() as JsonObject,
+ context: 'inline',
+ };
+ const sanitizedConfigs = schema.process([config], {
+ ignoreSchemaErrors: true,
+ valueTransform: (value, context) =>
+ context.visibility === 'secret' ? '' : value,
+ });
+
+ const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
+ configInfo.config = data;
+ configInfo.error = {
+ name: error.name,
+ message: error.message,
+ messages: error.messages as string[] | undefined,
+ stack: error.stack,
+ };
+ }
+
+ return configInfo;
+ }
+
+ public async listInfo(): Promise {
+ const operatingSystem = `${os.type} ${os.release} - ${os.platform}/${os.arch}`;
+ const nodeJsVersion = process.version;
+
+ /* eslint-disable-next-line no-restricted-syntax */
+ const paths = findPaths(__dirname);
+ const backstageFile = paths.resolveTargetRoot('backstage.json');
+ let backstageJson = undefined;
+ if (fs.existsSync(backstageFile)) {
+ const buffer = await fs.readFile(backstageFile);
+ backstageJson = JSON.parse(buffer.toString());
+ }
+
+ const lockfilePath = paths.resolveTargetRoot('yarn.lock');
+ const lockfile = await Lockfile.load(lockfilePath);
+
+ const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/'));
+
+ const infoDependencies: PackageDependency[] = [];
+ for (const dep of deps) {
+ const versions = new Set(lockfile.get(dep)!.map(i => i.version));
+ const infoDependency: PackageDependency = {
+ name: dep,
+ versions: [...versions].join(', '),
+ };
+ infoDependencies.push(infoDependency);
+ }
+
+ const info: DevToolsInfo = {
+ operatingSystem: operatingSystem ?? 'N/A',
+ nodeJsVersion: nodeJsVersion ?? 'N/A',
+ backstageVersion:
+ backstageJson && backstageJson.version ? backstageJson.version : 'N/A',
+ dependencies: infoDependencies,
+ };
+
+ return info;
+ }
+}
+
+export function isValidUrl(url: string): boolean {
+ try {
+ // eslint-disable-next-line no-new
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/plugins/devtools-backend/src/api/index.ts b/plugins/devtools-backend/src/api/index.ts
new file mode 100644
index 0000000000..ed3390b113
--- /dev/null
+++ b/plugins/devtools-backend/src/api/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DevToolsBackendApi } from './DevToolsBackendApi';
diff --git a/plugins/devtools-backend/src/index.ts b/plugins/devtools-backend/src/index.ts
new file mode 100644
index 0000000000..e1545e0022
--- /dev/null
+++ b/plugins/devtools-backend/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Backstage DevTools backend plugin that contains the API for retrieving package dependencies and current configuration which is used by the Backstage DevTools frontend plugin.
+ *
+ * @packageDocumentation
+ */
+
+export { DevToolsBackendApi } from './api';
+export * from './service/router';
diff --git a/plugins/devtools-backend/src/run.ts b/plugins/devtools-backend/src/run.ts
new file mode 100644
index 0000000000..d945aa13f0
--- /dev/null
+++ b/plugins/devtools-backend/src/run.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getRootLogger } from '@backstage/backend-common';
+import yn from 'yn';
+import { startStandaloneServer } from './service/standaloneServer';
+
+const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
+const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
+const logger = getRootLogger();
+
+startStandaloneServer({ port, enableCors, logger }).catch(err => {
+ logger.error(err);
+ process.exit(1);
+});
+
+process.on('SIGINT', () => {
+ logger.info('CTRL+C pressed; exiting.');
+ process.exit(0);
+});
diff --git a/plugins/devtools-backend/src/service/router.test.ts b/plugins/devtools-backend/src/service/router.test.ts
new file mode 100644
index 0000000000..f3c78242cf
--- /dev/null
+++ b/plugins/devtools-backend/src/service/router.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getVoidLogger } from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import express from 'express';
+import request from 'supertest';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+import { createRouter } from './router';
+
+const mockedAuthorize: jest.MockedFunction =
+ jest.fn();
+const mockedPermissionQuery: jest.MockedFunction<
+ PermissionEvaluator['authorizeConditional']
+> = jest.fn();
+
+const permissionEvaluator: PermissionEvaluator = {
+ authorize: mockedAuthorize,
+ authorizeConditional: mockedPermissionQuery,
+};
+
+describe('createRouter', () => {
+ let app: express.Express;
+
+ beforeAll(async () => {
+ const router = await createRouter({
+ logger: getVoidLogger(),
+ config: new ConfigReader({
+ healthCheck: {
+ endpoint: [
+ {
+ name: '',
+ type: '',
+ target: '',
+ },
+ ],
+ },
+ }),
+ permissions: permissionEvaluator,
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('GET /health', () => {
+ it('returns ok', async () => {
+ const response = await request(app).get('/health');
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({ status: 'ok' });
+ });
+ });
+});
diff --git a/plugins/devtools-backend/src/service/router.ts b/plugins/devtools-backend/src/service/router.ts
new file mode 100644
index 0000000000..a346ad3183
--- /dev/null
+++ b/plugins/devtools-backend/src/service/router.ts
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ AuthorizeResult,
+ PermissionEvaluator,
+} from '@backstage/plugin-permission-common';
+import {
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+
+import { Config } from '@backstage/config';
+import { DevToolsBackendApi } from '../api';
+import { Logger } from 'winston';
+import { NotAllowedError } from '@backstage/errors';
+import Router from 'express-promise-router';
+import { errorHandler } from '@backstage/backend-common';
+import express from 'express';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
+
+/** @public */
+export interface RouterOptions {
+ devToolsBackendApi?: DevToolsBackendApi;
+ logger: Logger;
+ config: Config;
+ permissions: PermissionEvaluator;
+}
+
+/** @public */
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const { logger, config, permissions } = options;
+
+ const devToolsBackendApi =
+ options.devToolsBackendApi || new DevToolsBackendApi(logger, config);
+
+ const router = Router();
+ router.use(express.json());
+
+ router.get('/health', (_req, res) => {
+ res.status(200).json({ status: 'ok' });
+ });
+
+ router.get('/info', async (req, response) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize(
+ [{ permission: devToolsInfoReadPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ const info = await devToolsBackendApi.listInfo();
+
+ response.status(200).json(info);
+ });
+
+ router.get('/config', async (req, response) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize(
+ [{ permission: devToolsConfigReadPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ const configList = await devToolsBackendApi.listConfig();
+
+ response.status(200).json(configList);
+ });
+
+ router.get('/external-dependencies', async (req, response) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize(
+ [{ permission: devToolsExternalDependenciesReadPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ const health = await devToolsBackendApi.listExternalDependencyDetails();
+
+ response.status(200).json(health);
+ });
+
+ router.use(errorHandler());
+ return router;
+}
diff --git a/plugins/devtools-backend/src/service/standaloneServer.ts b/plugins/devtools-backend/src/service/standaloneServer.ts
new file mode 100644
index 0000000000..d2fdce169d
--- /dev/null
+++ b/plugins/devtools-backend/src/service/standaloneServer.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ServerTokenManager,
+ SingleHostDiscovery,
+ createServiceBuilder,
+ loadBackendConfig,
+} from '@backstage/backend-common';
+
+import { Logger } from 'winston';
+import { Server } from 'http';
+import { ServerPermissionClient } from '@backstage/plugin-permission-node';
+import { createRouter } from './router';
+
+export interface ServerOptions {
+ port: number;
+ enableCors: boolean;
+ logger: Logger;
+}
+
+export async function startStandaloneServer(
+ options: ServerOptions,
+): Promise {
+ const logger = options.logger.child({ service: 'devtools-backend-backend' });
+ const config = await loadBackendConfig({ logger, argv: process.argv });
+ const discovery = SingleHostDiscovery.fromConfig(config);
+ const tokenManager = ServerTokenManager.fromConfig(config, {
+ logger,
+ });
+ const permissions = ServerPermissionClient.fromConfig(config, {
+ discovery,
+ tokenManager,
+ });
+ logger.debug('Starting application server...');
+ const router = await createRouter({
+ logger,
+ config,
+ permissions,
+ });
+
+ let service = createServiceBuilder(module)
+ .setPort(options.port)
+ .addRouter('/devtools-backend', router);
+ if (options.enableCors) {
+ service = service.enableCors({ origin: 'http://localhost:3000' });
+ }
+
+ return await service.start().catch(err => {
+ logger.error(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
diff --git a/plugins/devtools-backend/src/setupTests.ts b/plugins/devtools-backend/src/setupTests.ts
new file mode 100644
index 0000000000..813cdeaae3
--- /dev/null
+++ b/plugins/devtools-backend/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
diff --git a/plugins/devtools-backend/src/util/Lockfile.ts b/plugins/devtools-backend/src/util/Lockfile.ts
new file mode 100644
index 0000000000..52dbb847bc
--- /dev/null
+++ b/plugins/devtools-backend/src/util/Lockfile.ts
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs-extra';
+import semver from 'semver';
+import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
+import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
+
+const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
+
+type LockfileData = {
+ [entry: string]: {
+ version: string;
+ resolved?: string;
+ integrity?: string;
+ dependencies?: { [name: string]: string };
+ };
+};
+
+type LockfileQueryEntry = {
+ range: string;
+ version: string;
+};
+
+/** Entries that have an invalid version range, for example an npm tag */
+type AnalyzeResultInvalidRange = {
+ name: string;
+ range: string;
+};
+
+/** Entries that can be deduplicated by bumping to an existing higher version */
+type AnalyzeResultNewVersion = {
+ name: string;
+ range: string;
+ oldVersion: string;
+ newVersion: string;
+};
+
+/** Entries that would need a dependency update in package.json to be deduplicated */
+type AnalyzeResultNewRange = {
+ name: string;
+ oldRange: string;
+ newRange: string;
+ oldVersion: string;
+ newVersion: string;
+};
+
+type AnalyzeResult = {
+ invalidRanges: AnalyzeResultInvalidRange[];
+ newVersions: AnalyzeResultNewVersion[];
+ newRanges: AnalyzeResultNewRange[];
+};
+
+function parseLockfile(lockfileContents: string) {
+ try {
+ return {
+ object: parseSyml(lockfileContents),
+ type: 'success',
+ };
+ } catch (err) {
+ return {
+ object: null,
+ type: err,
+ };
+ }
+}
+
+// the new yarn header is handled out of band of the parsing
+// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
+const NEW_HEADER = `${[
+ `# This file is generated by running "yarn install" inside your project.\n`,
+ `# Manual changes might be lost - proceed with caution!\n`,
+].join(``)}\n`;
+
+function stringifyLockfile(data: LockfileData, legacy: boolean) {
+ return legacy
+ ? legacyStringifyLockfile(data)
+ : NEW_HEADER + stringifySyml(data);
+}
+// taken from yarn parser package
+// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
+const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
+
+// these are special top level yarn keys.
+// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9
+const SPECIAL_OBJECT_KEYS = [
+ `__metadata`,
+ `version`,
+ `resolution`,
+ `dependencies`,
+ `peerDependencies`,
+ `dependenciesMeta`,
+ `peerDependenciesMeta`,
+ `binaries`,
+];
+
+export class Lockfile {
+ static async load(path: string) {
+ const lockfileContents = await fs.readFile(path, 'utf8');
+ const legacy = LEGACY_REGEX.test(lockfileContents);
+ const lockfile = parseLockfile(lockfileContents);
+ if (lockfile.type !== 'success') {
+ throw new Error(`Failed yarn.lock parse with ${lockfile.type}`);
+ }
+
+ const data = lockfile.object as LockfileData;
+ const packages = new Map();
+
+ for (const [key, value] of Object.entries(data)) {
+ if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
+
+ const [, name, range] = ENTRY_PATTERN.exec(key) ?? [];
+ if (!name) {
+ throw new Error(`Failed to parse yarn.lock entry '${key}'`);
+ }
+
+ let queries = packages.get(name);
+ if (!queries) {
+ queries = [];
+ packages.set(name, queries);
+ }
+ queries.push({ range, version: value.version });
+ }
+
+ return new Lockfile(path, packages, data, legacy);
+ }
+
+ private constructor(
+ private readonly path: string,
+ private readonly packages: Map,
+ private readonly data: LockfileData,
+ private readonly legacy: boolean = false,
+ ) {}
+
+ /** Get the entries for a single package in the lockfile */
+ get(name: string): LockfileQueryEntry[] | undefined {
+ return this.packages.get(name);
+ }
+
+ /** Returns the name of all packages available in the lockfile */
+ keys(): IterableIterator {
+ return this.packages.keys();
+ }
+
+ /** Analyzes the lockfile to identify possible actions and warnings for the entries */
+ analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult {
+ const { filter } = options ?? {};
+ const result: AnalyzeResult = {
+ invalidRanges: [],
+ newVersions: [],
+ newRanges: [],
+ };
+
+ for (const [name, allEntries] of this.packages) {
+ if (filter && !filter(name)) {
+ continue;
+ }
+
+ // Get rid of and signal any invalid ranges upfront
+ const invalid = allEntries.filter(e => !semver.validRange(e.range));
+ result.invalidRanges.push(
+ ...invalid.map(({ range }) => ({ name, range })),
+ );
+
+ // Grab all valid entries, if there aren't at least 2 different valid ones we're done
+ const entries = allEntries.filter(e => semver.validRange(e.range));
+ if (entries.length < 2) {
+ continue;
+ }
+
+ // Find all versions currently in use
+ const versions = Array.from(new Set(entries.map(e => e.version))).sort(
+ (v1, v2) => semver.rcompare(v1, v2),
+ );
+
+ // If we're not using at least 2 different versions we're done
+ if (versions.length < 2) {
+ continue;
+ }
+
+ const acceptedVersions = new Set();
+ for (const { version, range } of entries) {
+ // Finds the highest matching version from the the known versions
+ // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one
+ const acceptedVersion = versions.find(v => semver.satisfies(v, range));
+ if (!acceptedVersion) {
+ throw new Error(
+ `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
+ );
+ }
+
+ if (acceptedVersion !== version) {
+ result.newVersions.push({
+ name,
+ range,
+ newVersion: acceptedVersion,
+ oldVersion: version,
+ });
+ }
+
+ acceptedVersions.add(acceptedVersion);
+ }
+
+ // If all ranges were able to accept the same version, we're done
+ if (acceptedVersions.size === 1) {
+ continue;
+ }
+
+ // Find the max version that we may want bump older packages to
+ const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0];
+ // Find all existing ranges that satisfy the new max version, and pick the one that
+ // results in the highest minimum allowed version, usually being the more specific one
+ const maxEntry = entries
+ .filter(e => semver.satisfies(maxVersion, e.range))
+ .map(e => ({ e, min: semver.minVersion(e.range) }))
+ .filter(p => p.min)
+ .sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e;
+ if (!maxEntry) {
+ throw new Error(
+ `No entry found that satisfies max version '${maxVersion}'`,
+ );
+ }
+
+ // Find all entries that don't satisfy the max version
+ for (const { version, range } of entries) {
+ if (semver.satisfies(maxVersion, range)) {
+ continue;
+ }
+
+ result.newRanges.push({
+ name,
+ oldRange: range,
+ newRange: maxEntry.range,
+ oldVersion: version,
+ newVersion: maxVersion,
+ });
+ }
+ }
+
+ return result;
+ }
+
+ remove(name: string, range: string): boolean {
+ const query = `${name}@${range}`;
+ const existed = Boolean(this.data[query]);
+ delete this.data[query];
+
+ const newEntries = this.packages.get(name)?.filter(e => e.range !== range);
+ if (newEntries) {
+ this.packages.set(name, newEntries);
+ }
+
+ return existed;
+ }
+
+ /** Modifies the lockfile by bumping packages to the suggested versions */
+ replaceVersions(results: AnalyzeResultNewVersion[]) {
+ for (const { name, range, oldVersion, newVersion } of results) {
+ const query = `${name}@${range}`;
+
+ // Update the backing data
+ const entryData = this.data[query];
+ if (!entryData) {
+ throw new Error(`No entry data for ${query}`);
+ }
+ if (entryData.version !== oldVersion) {
+ throw new Error(
+ `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
+ );
+ }
+
+ // Modifying the data in the entry is not enough, we need to reference an existing version object
+ const matchingEntry = Object.entries(this.data).find(
+ ([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion,
+ );
+ if (!matchingEntry) {
+ throw new Error(
+ `No matching entry found for ${name} at version ${newVersion}`,
+ );
+ }
+ this.data[query] = matchingEntry[1];
+
+ // Update our internal data structure
+ const entry = this.packages.get(name)?.find(e => e.range === range);
+ if (!entry) {
+ throw new Error(`No entry data for ${query}`);
+ }
+ if (entry.version !== oldVersion) {
+ throw new Error(
+ `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
+ );
+ }
+ entry.version = newVersion;
+ }
+ }
+
+ async save() {
+ await fs.writeFile(this.path, this.toString(), 'utf8');
+ }
+
+ toString() {
+ return stringifyLockfile(this.data, this.legacy);
+ }
+}
diff --git a/plugins/devtools-common/.eslintrc.js b/plugins/devtools-common/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/devtools-common/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md
new file mode 100644
index 0000000000..79c40a24c1
--- /dev/null
+++ b/plugins/devtools-common/CHANGELOG.md
@@ -0,0 +1,7 @@
+# @backstage/plugin-devtools-common
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
diff --git a/plugins/devtools-common/README.md b/plugins/devtools-common/README.md
new file mode 100644
index 0000000000..5be5e58773
--- /dev/null
+++ b/plugins/devtools-common/README.md
@@ -0,0 +1,3 @@
+# DevTools Common
+
+Common types and permissions for the DevTools plugin.
diff --git a/plugins/devtools-common/api-report.md b/plugins/devtools-common/api-report.md
new file mode 100644
index 0000000000..7e38ff05b0
--- /dev/null
+++ b/plugins/devtools-common/api-report.md
@@ -0,0 +1,72 @@
+## API Report File for "@backstage/plugin-devtools-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BasicPermission } from '@backstage/plugin-permission-common';
+import { JsonValue } from '@backstage/types';
+
+// @public (undocumented)
+export type ConfigError = {
+ name: string;
+ message: string;
+ messages?: string[];
+ stack?: string;
+};
+
+// @public (undocumented)
+export type ConfigInfo = {
+ config?: JsonValue;
+ error?: ConfigError;
+};
+
+// @public (undocumented)
+export const devToolsAdministerPermission: BasicPermission;
+
+// @public (undocumented)
+export const devToolsConfigReadPermission: BasicPermission;
+
+// @public (undocumented)
+export const devToolsExternalDependenciesReadPermission: BasicPermission;
+
+// @public (undocumented)
+export type DevToolsInfo = {
+ operatingSystem: string;
+ nodeJsVersion: string;
+ backstageVersion: string;
+ dependencies: PackageDependency[];
+};
+
+// @public (undocumented)
+export const devToolsInfoReadPermission: BasicPermission;
+
+// @public (undocumented)
+export type Endpoint = {
+ name: string;
+ type: string;
+ target: string;
+};
+
+// @public (undocumented)
+export type ExternalDependency = {
+ name: string;
+ type: string;
+ target: string;
+ status: string;
+ error?: string;
+};
+
+// @public (undocumented)
+export enum ExternalDependencyStatus {
+ // (undocumented)
+ healthy = 'Healthy',
+ // (undocumented)
+ unhealthy = 'Unhealthy',
+}
+
+// @public (undocumented)
+export type PackageDependency = {
+ name: string;
+ versions: string;
+};
+```
diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json
new file mode 100644
index 0000000000..ba48da49ad
--- /dev/null
+++ b/plugins/devtools-common/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@backstage/plugin-devtools-common",
+ "description": "Common functionalities for the devtools plugin",
+ "version": "0.1.0-next.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "module": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "common-library"
+ },
+ "scripts": {
+ "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/plugin-permission-common": "workspace:^",
+ "@backstage/types": "workspace:^"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/devtools-common/src/index.ts b/plugins/devtools-common/src/index.ts
new file mode 100644
index 0000000000..be7426394b
--- /dev/null
+++ b/plugins/devtools-common/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Common types and permissions for the DevTools frontend and backend plugins.
+ *
+ * @packageDocumentation
+ */
+
+export * from './types';
+export * from './permissions';
diff --git a/plugins/devtools-common/src/permissions.ts b/plugins/devtools-common/src/permissions.ts
new file mode 100644
index 0000000000..ae62cfdc19
--- /dev/null
+++ b/plugins/devtools-common/src/permissions.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createPermission } from '@backstage/plugin-permission-common';
+
+/**
+ * @public
+ */
+export const devToolsAdministerPermission = createPermission({
+ name: 'devtools.administer',
+ attributes: { action: 'read' },
+});
+
+/**
+ * @public
+ */
+export const devToolsInfoReadPermission = createPermission({
+ name: 'devtools.info',
+ attributes: { action: 'read' },
+});
+
+/**
+ * @public
+ */
+export const devToolsConfigReadPermission = createPermission({
+ name: 'devtools.config',
+ attributes: { action: 'read' },
+});
+
+/**
+ * @public
+ */
+export const devToolsExternalDependenciesReadPermission = createPermission({
+ name: 'devtools.external-dependencies',
+ attributes: { action: 'read' },
+});
diff --git a/plugins/devtools-common/src/setupTests.ts b/plugins/devtools-common/src/setupTests.ts
new file mode 100644
index 0000000000..813cdeaae3
--- /dev/null
+++ b/plugins/devtools-common/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
diff --git a/plugins/devtools-common/src/types.ts b/plugins/devtools-common/src/types.ts
new file mode 100644
index 0000000000..d08232b155
--- /dev/null
+++ b/plugins/devtools-common/src/types.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { JsonValue } from '@backstage/types';
+
+/** @public */
+export type Endpoint = {
+ name: string;
+ type: string;
+ target: string;
+};
+
+/** @public */
+export type ExternalDependency = {
+ name: string;
+ type: string;
+ target: string;
+ status: string;
+ error?: string;
+};
+
+/** @public */
+export type DevToolsInfo = {
+ operatingSystem: string;
+ nodeJsVersion: string;
+ backstageVersion: string;
+ dependencies: PackageDependency[];
+};
+
+/** @public */
+export type PackageDependency = {
+ name: string;
+ versions: string;
+};
+
+/** @public */
+export enum ExternalDependencyStatus {
+ healthy = 'Healthy',
+ unhealthy = 'Unhealthy',
+}
+
+/** @public */
+export type ConfigInfo = {
+ config?: JsonValue;
+ error?: ConfigError;
+};
+
+/** @public */
+export type ConfigError = {
+ name: string;
+ message: string;
+ messages?: string[];
+ stack?: string;
+};
diff --git a/plugins/devtools/.eslintrc.js b/plugins/devtools/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/devtools/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md
new file mode 100644
index 0000000000..0fe41704c6
--- /dev/null
+++ b/plugins/devtools/CHANGELOG.md
@@ -0,0 +1,16 @@
+# @backstage/plugin-devtools
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0-next.0
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md
new file mode 100644
index 0000000000..fc9f1f7312
--- /dev/null
+++ b/plugins/devtools/README.md
@@ -0,0 +1,375 @@
+# DevTools
+
+Welcome to the DevTools plugin! This plugin helps elevate useful information about a Backstage instance through the frontend, which can be helpful for an integrator for troubleshooting, reviewing, and understanding their installation.
+
+## Features
+
+The DevTools plugin comes with two tabs out of the box.
+
+### Info
+
+Lists helpful information about your current running Backstage instance such as: OS, NodeJS version, Backstage version, and package versions.
+
+
+
+### Config
+
+Lists the configuration being used by your current running Backstage instance.
+
+
+
+## Optional Features
+
+The DevTools plugin can be setup with other tabs with additional helpful features.
+
+### External Dependencies
+
+Lists the status of configured External Dependencies based on your current running Backstage instance's ability to reach them.
+
+
+
+## Setup
+
+The following sections will help you get the DevTools plugin setup and running.
+
+### Backend
+
+You need to setup the [DevTools backend plugin](../devtools-backend/README.md) before you move forward with any of the following steps if you haven't already.
+
+### Frontend
+
+To setup the DevTools frontend you'll need to do the following steps:
+
+1. First we need to add the `@backstage/plugin-devtools` package to your frontend app:
+
+ ```sh
+ # From your Backstage root directory
+ yarn add --cwd packages/app @backstage/plugin-devtools
+ ```
+
+2. Now open the `packages/app/src/App.tsx` file
+3. Then after all the import statements add the following line:
+
+ ```ts
+ import { DevToolsPage } from '@backstage/plugin-devtools';
+ ```
+
+4. In this same file just before the closing ` FlatRoutes>`, this will be near the bottom of the file, add this line:
+
+ ```ts
+ } />
+ ```
+
+5. Next open the `packages/app/src/components/Root/Root.tsx` file
+6. We want to add this icon import after all the existing import statements:
+
+ ```ts
+ import BuildIcon from '@material-ui/icons/Build';
+ ```
+
+7. Then add this line just after the `` line:
+
+ ```ts
+
+ ```
+
+8. Now run `yarn dev` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info)
+
+## Customizing
+
+The DevTools plugin has been designed so that you can customize the tabs to suite your needs. You may only want some or none of the out of the box tabs or you may want to add your own. The following sections explains how to do that (assuming you've already done the [setup steps](#setup)). As part of this example we'll also be showing how you can add the optional [External Dependencies](#external-dependencies) tab.
+
+1. In the `packages/app/src/components` folder create a new sub-folder called `devtools`
+2. Then in this new `devtools` folder add a file called `CustomDevToolsPage.tsx`
+3. In the `CustomDevToolsPage.tsx` file add the following content:
+
+ ```tsx
+ import {
+ ConfigContent,
+ ExternalDependenciesContent,
+ InfoContent,
+ } from '@backstage/plugin-devtools';
+ import { DevToolsLayout } from '@backstage/plugin-devtools';
+ import React from 'react';
+
+ export const DevToolsPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ export const customDevToolsPage = ;
+ ```
+
+4. Now open the `packages/app/src/App.tsx` file and add the following import after all the existing import statements:
+
+ ```ts
+ import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
+ ```
+
+5. Then we need to adjust our route as follows
+
+ ```diff
+ - } />
+ + } >
+ + {customDevToolsPage}
+ +
+ ```
+
+6. Now run `yarn dev` from the root of your project. When you go to the DevTools you'll now see you have a third tab for [External Dependencies](#external-dependencies)
+
+With this setup you can add or remove the tabs as you'd like or add your own simply by editing your `CustomDevToolsPage.tsx` file
+
+## Permissions
+
+The DevTools plugin supports the [permissions framework](https://backstage.io/docs/permissions/overview), the following sections outline how you can use them with the assumption that you have the permissions framework setup and working.
+
+**Note:** These sections are intended as guidance and are completely optional. The DevTools plugin will work with the permission framework off or on without any specific policy setup.
+
+### Secure Sidebar Option
+
+To use the permission framework to secure the DevTools sidebar option you'll want to do the following:
+
+1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app:
+
+ ```sh
+ # From your Backstage root directory
+ yarn add --cwd packages/app @backstage/plugin-devtools
+ ```
+
+2. Then open the `packages/app/src/components/Root/Root.tsx` file
+3. The add these imports after all the existing import statements:
+
+ ```ts
+ import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common';
+ import { RequirePermission } from '@backstage/plugin-permission-react';
+ ```
+
+4. Then make the following change:
+
+ ```diff
+ -
+ + >}>
+ +
+ +
+ ```
+
+### Secure the DevTools Route
+
+To use the permission framework to secure the DevTools route you'll want to do the following:
+
+1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app (skip this step if you've already done this):
+
+ ```sh
+ # From your Backstage root directory
+ yarn add --cwd packages/app @backstage/plugin-devtools-common
+ ```
+
+2. Then open the `packages/app/src/App.tsx` file
+3. The add this import after all the existing import statements:
+
+ ```ts
+ import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common';
+ ```
+
+4. Then make the following change:
+
+ ```diff
+ - } />
+ +
+ +
+ +
+ + }
+ + />
+ ```
+
+Note: if you are using a `customDevToolsPage` as per the [Customizing](#customizing) documentation the changes for Step 4 will be:
+
+```diff
+- } />
++
++
++
++ }
++ >
++ {customDevToolsPage}
++
+```
+
+### Permission Policy
+
+Here is an example permission policy that you might use to secure the DevTools plugin:
+
+```ts
+// packages/backend/src/plugins/permission.ts
+
+class TestPermissionPolicy implements PermissionPolicy {
+ async handle(request: PolicyQuery): Promise {
+ if (isPermission(request.permission, devToolsAdministerPermission)) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ if (isPermission(request.permission, devToolsInfoReadPermission)) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ if (isPermission(request.permission, devToolsConfigReadPermission)) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ if (
+ isPermission(
+ request.permission,
+ devToolsExternalDependenciesReadPermission,
+ )
+ ) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ return { result: AuthorizeResult.ALLOW };
+ }
+}
+```
+
+To use this policy you'll need to make sure to add the `@backstage/plugin-devtools-common` package to your backend you can do that by running this command:
+
+```sh
+# From your Backstage root directory
+yarn add --cwd packages/backend @backstage/plugin-devtools-common
+```
+
+You'll also need to add these imports:
+
+```ts
+import {
+ devToolsAdministerPermission,
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+```
+
+**Note:** The group "group:default/backstage-admins" is simply an example and does not exist. You can point this to any group you have in your catalog instead.
+
+### Customizing with Permissions
+
+If you followed the [Customizing](#customizing) documentation and want to use permission there this is what your `CustomDevToolsPage.tsx` would look like:
+
+```tsx
+import {
+ ConfigContent,
+ ExternalDependenciesContent,
+ InfoContent,
+} from '@backstage/plugin-devtools';
+import { DevToolsLayout } from '@backstage/plugin-devtools';
+import {
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+import { RequirePermission } from '@backstage/plugin-permission-react';
+import React from 'react';
+
+const DevToolsPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const customDevToolsPage = ;
+```
+
+## Configuration
+
+The following sections outline the configuration for the DevTools plugin
+
+### External Dependencies Configuration
+
+If you decide to use the External Dependencies tab then you'll need to setup the configuration for it in your `app-config.yaml`, if there is no config setup then the tab will be empty. Here's an example:
+
+```yaml
+devTools:
+ externalDependencies:
+ endpoints:
+ - name: 'Google'
+ type: 'fetch'
+ target: 'https://google.ca'
+ - name: 'Google Public DNS'
+ type: 'ping'
+ target: '8.8.8.8'
+```
+
+Configuration details:
+
+- `endpoints` is an array
+- `name` is the friendly name for your endpoint
+- `type` can be either `ping` or `fetch` and will perform the respective action on the `target`
+- `target` is either a URL or server that you want to trigger a `type` action on
diff --git a/plugins/devtools/api-report.md b/plugins/devtools/api-report.md
new file mode 100644
index 0000000000..c9cfbf77e5
--- /dev/null
+++ b/plugins/devtools/api-report.md
@@ -0,0 +1,57 @@
+## API Report File for "@backstage/plugin-devtools"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { default as default_2 } from 'react';
+import { RouteRef } from '@backstage/core-plugin-api';
+import { TabProps } from '@material-ui/core';
+
+// @public (undocumented)
+export const ConfigContent: () => JSX.Element;
+
+// @public
+export const DevToolsLayout: {
+ ({ children }: DevToolsLayoutProps): JSX.Element;
+ Route: (props: SubRoute) => null;
+};
+
+// @public (undocumented)
+export type DevToolsLayoutProps = {
+ children?: default_2.ReactNode;
+};
+
+// @public (undocumented)
+export const DevToolsPage: () => JSX.Element;
+
+// @public (undocumented)
+export const devToolsPlugin: BackstagePlugin<
+ {
+ root: RouteRef;
+ },
+ {},
+ {}
+>;
+
+// @public (undocumented)
+export const ExternalDependenciesContent: () => JSX.Element;
+
+// @public (undocumented)
+export const InfoContent: () => JSX.Element;
+
+// @public (undocumented)
+export type SubRoute = {
+ path: string;
+ title: string;
+ children: JSX.Element;
+ tabProps?: TabProps<
+ default_2.ElementType,
+ {
+ component?: default_2.ElementType;
+ }
+ >;
+};
+```
diff --git a/plugins/devtools/dev/index.tsx b/plugins/devtools/dev/index.tsx
new file mode 100644
index 0000000000..00c966a801
--- /dev/null
+++ b/plugins/devtools/dev/index.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { createDevApp } from '@backstage/dev-utils';
+import { devToolsPlugin, DevToolsPage } from '../src/plugin';
+
+createDevApp()
+ .registerPlugin(devToolsPlugin)
+ .addPage({
+ element: ,
+ title: 'Root Page',
+ path: '/devtools',
+ })
+ .render();
diff --git a/plugins/devtools/docs/devtools-config-tab.png b/plugins/devtools/docs/devtools-config-tab.png
new file mode 100644
index 0000000000..b5c22f691b
Binary files /dev/null and b/plugins/devtools/docs/devtools-config-tab.png differ
diff --git a/plugins/devtools/docs/devtools-external-dependencies.png b/plugins/devtools/docs/devtools-external-dependencies.png
new file mode 100644
index 0000000000..9356d83ef5
Binary files /dev/null and b/plugins/devtools/docs/devtools-external-dependencies.png differ
diff --git a/plugins/devtools/docs/devtools-info-tab.png b/plugins/devtools/docs/devtools-info-tab.png
new file mode 100644
index 0000000000..739045a77f
Binary files /dev/null and b/plugins/devtools/docs/devtools-info-tab.png differ
diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json
new file mode 100644
index 0000000000..5997f79845
--- /dev/null
+++ b/plugins/devtools/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@backstage/plugin-devtools",
+ "version": "0.1.0-next.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/devtools"
+ },
+ "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": "workspace:^",
+ "@backstage/core-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-devtools-common": "workspace:^",
+ "@backstage/plugin-permission-react": "workspace:^",
+ "@backstage/theme": "workspace:^",
+ "@backstage/types": "workspace:^",
+ "@material-ui/core": "^4.9.13",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "^4.0.0-alpha.57",
+ "react-json-view": "^1.21.3",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "react": "^16.13.1 || ^17.0.0",
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@backstage/core-app-api": "workspace:^",
+ "@backstage/dev-utils": "workspace:^",
+ "@backstage/test-utils": "workspace:^",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/node": "*",
+ "cross-fetch": "^3.1.5",
+ "msw": "^0.47.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts
new file mode 100644
index 0000000000..ff58a4ea16
--- /dev/null
+++ b/plugins/devtools/src/api/DevToolsApi.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createApiRef } from '@backstage/core-plugin-api';
+import {
+ ConfigInfo,
+ DevToolsInfo,
+ ExternalDependency,
+} from '@backstage/plugin-devtools-common';
+
+export const devToolsApiRef = createApiRef({
+ id: 'plugin.devtools.service',
+});
+
+export interface DevToolsApi {
+ getConfig(): Promise;
+ getExternalDependencies(): Promise;
+ getInfo(): Promise;
+}
diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts
new file mode 100644
index 0000000000..0be8294b3f
--- /dev/null
+++ b/plugins/devtools/src/api/DevToolsClient.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
+import {
+ ConfigInfo,
+ DevToolsInfo,
+ ExternalDependency,
+} from '@backstage/plugin-devtools-common';
+import { ResponseError } from '@backstage/errors';
+import { DevToolsApi } from './DevToolsApi';
+
+export class DevToolsClient implements DevToolsApi {
+ private readonly discoveryApi: DiscoveryApi;
+ private readonly identityApi: IdentityApi;
+
+ public constructor(options: {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+ }) {
+ this.discoveryApi = options.discoveryApi;
+ this.identityApi = options.identityApi;
+ }
+
+ public async getConfig(): Promise {
+ const urlSegment = 'config';
+
+ const configInfo = await this.get(urlSegment);
+ return configInfo;
+ }
+
+ public async getExternalDependencies(): Promise<
+ ExternalDependency[] | undefined
+ > {
+ const urlSegment = 'external-dependencies';
+
+ const externalDependencies = await this.get<
+ ExternalDependency[] | undefined
+ >(urlSegment);
+ return externalDependencies;
+ }
+
+ public async getInfo(): Promise {
+ const urlSegment = 'info';
+
+ const info = await this.get(urlSegment);
+ return info;
+ }
+
+ private async get(path: string): Promise {
+ const baseUrl = `${await this.discoveryApi.getBaseUrl('devtools')}/`;
+ const url = new URL(path, baseUrl);
+
+ const { token } = await this.identityApi.getCredentials();
+ const response = await fetch(url.toString(), {
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ });
+
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ }
+
+ return response.json() as Promise;
+ }
+}
diff --git a/plugins/devtools/src/api/index.ts b/plugins/devtools/src/api/index.ts
new file mode 100644
index 0000000000..6bea0a07aa
--- /dev/null
+++ b/plugins/devtools/src/api/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './DevToolsApi';
+export * from './DevToolsClient';
diff --git a/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx
new file mode 100644
index 0000000000..66129b5dae
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Progress, WarningPanel } from '@backstage/core-components';
+import {
+ Box,
+ createStyles,
+ makeStyles,
+ Paper,
+ Theme,
+ Typography,
+ useTheme,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import ReactJson from 'react-json-view';
+import { useConfig } from '../../../hooks';
+import { ConfigError } from '@backstage/plugin-devtools-common';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ warningStyle: {
+ paddingBottom: theme.spacing(2),
+ },
+ paperStyle: {
+ padding: theme.spacing(2),
+ },
+ }),
+);
+
+export const WarningContent = ({ error }: { error: ConfigError }) => {
+ if (!error.messages) {
+ return {error.message};
+ }
+
+ const messages = error.messages as string[];
+
+ return (
+
+ {messages.map(message => (
+ {message}
+ ))}
+
+ );
+};
+
+/** @public */
+export const ConfigContent = () => {
+ const classes = useStyles();
+ const theme = useTheme();
+ const { configInfo, loading, error } = useConfig();
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ }
+
+ if (!configInfo) {
+ return Unable to load config data;
+ }
+
+ return (
+
+ {configInfo && configInfo.error && (
+
+
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/ConfigContent/index.ts b/plugins/devtools/src/components/Content/ConfigContent/index.ts
new file mode 100644
index 0000000000..b1cc671de8
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ConfigContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ConfigContent } from './ConfigContent';
diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx
new file mode 100644
index 0000000000..a5dd825fe5
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Progress,
+ StatusError,
+ StatusOK,
+ StatusWarning,
+ Table,
+ TableColumn,
+} from '@backstage/core-components';
+import { ExternalDependency } from '@backstage/plugin-devtools-common';
+import {
+ Box,
+ createStyles,
+ Grid,
+ makeStyles,
+ Paper,
+ Theme,
+ Typography,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import { useExternalDependencies } from '../../../hooks';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ paperStyle: {
+ padding: theme.spacing(2),
+ },
+ }),
+);
+
+export const getExternalDependencyStatus = (
+ result: Partial | undefined,
+) => {
+ switch (result?.status) {
+ case 'Healthy':
+ return (
+
+ {result.status}
+
+ );
+ case 'Unhealthy':
+ return (
+
+ {`${result.status}`}
+
+ );
+ case undefined:
+ default:
+ return (
+
+ Unknown
+
+ );
+ }
+};
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ width: 'auto',
+ field: 'name',
+ },
+ {
+ title: 'Target',
+ width: 'auto',
+ field: 'target',
+ },
+ {
+ title: 'Type',
+ width: 'auto',
+ field: 'type',
+ },
+ {
+ title: 'Status',
+ width: 'auto',
+ render: (row: Partial) => (
+
+
+
+ {getExternalDependencyStatus(row)}
+
+
+ {row.error && {row.error}}
+
+ ),
+ },
+];
+
+/** @public */
+export const ExternalDependenciesContent = () => {
+ const classes = useStyles();
+ const { externalDependencies, loading, error } = useExternalDependencies();
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ }
+
+ if (!externalDependencies || externalDependencies.length === 0) {
+ return (
+
+
+ No external dependencies found
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts b/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts
new file mode 100644
index 0000000000..bf747f3065
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ExternalDependenciesContent } from './ExternalDependenciesContent';
diff --git a/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx
new file mode 100644
index 0000000000..32f35600f2
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SvgIcon, SvgIconProps } from '@material-ui/core';
+
+import React from 'react';
+
+export const BackstageLogoIcon = (props: SvgIconProps) => (
+
+
+
+);
diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx
new file mode 100644
index 0000000000..7a0ebb3e06
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Progress } from '@backstage/core-components';
+import {
+ Avatar,
+ Box,
+ createStyles,
+ Divider,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ makeStyles,
+ Paper,
+ Theme,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import { useInfo } from '../../../hooks';
+import { InfoDependenciesTable } from './InfoDependenciesTable';
+import DescriptionIcon from '@material-ui/icons/Description';
+import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard';
+import { BackstageLogoIcon } from './BackstageLogoIcon';
+import FileCopyIcon from '@material-ui/icons/FileCopy';
+import { DevToolsInfo } from '@backstage/plugin-devtools-common';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ paperStyle: {
+ marginBottom: theme.spacing(2),
+ },
+ flexContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ padding: 0,
+ },
+ copyButton: {
+ float: 'left',
+ margin: theme.spacing(2),
+ },
+ }),
+);
+
+const copyToClipboard = ({ about }: { about: DevToolsInfo | undefined }) => {
+ if (about) {
+ let formatted = `OS: ${about.operatingSystem}\nnode: ${about.nodeJsVersion}\nbackstage: ${about.backstageVersion}\nDependencies:\n`;
+ const deps = about.dependencies;
+ for (const key in deps) {
+ if (Object.prototype.hasOwnProperty.call(deps, key)) {
+ formatted = `${formatted} ${deps[key].name}: ${deps[key].versions}\n`;
+ }
+ }
+ window.navigator.clipboard.writeText(formatted);
+ }
+};
+
+/** @public */
+export const InfoContent = () => {
+ const classes = useStyles();
+ const { about, loading, error } = useInfo();
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ copyToClipboard({ about });
+ }}
+ className={classes.copyButton}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx
new file mode 100644
index 0000000000..e1ab59f279
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Table, TableColumn } from '@backstage/core-components';
+import { PackageDependency } from '@backstage/plugin-devtools-common';
+
+import React from 'react';
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ width: 'auto',
+ field: 'name',
+ defaultSort: 'asc',
+ },
+ {
+ title: 'Versions',
+ width: 'auto',
+ field: 'versions',
+ },
+];
+
+export const InfoDependenciesTable = ({
+ infoDependencies,
+}: {
+ infoDependencies: PackageDependency[] | undefined;
+}) => {
+ return (
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/InfoContent/index.ts b/plugins/devtools/src/components/Content/InfoContent/index.ts
new file mode 100644
index 0000000000..6d5bad53f8
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { InfoContent } from './InfoContent';
diff --git a/plugins/devtools/src/components/Content/index.ts b/plugins/devtools/src/components/Content/index.ts
new file mode 100644
index 0000000000..f5bd64f7c0
--- /dev/null
+++ b/plugins/devtools/src/components/Content/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './ConfigContent';
+export * from './InfoContent';
+export * from './ExternalDependenciesContent';
diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
new file mode 100644
index 0000000000..a5e4f2439b
--- /dev/null
+++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ devToolsConfigReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+
+import { ConfigContent } from '../Content/ConfigContent';
+import { DevToolsLayout } from '../DevToolsLayout';
+import { InfoContent } from '../Content/InfoContent';
+import React from 'react';
+import { RequirePermission } from '@backstage/plugin-permission-react';
+
+/** @public */
+export const DefaultDevToolsPage = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/index.ts b/plugins/devtools/src/components/DefaultDevToolsPage/index.ts
new file mode 100644
index 0000000000..133dcf6e3a
--- /dev/null
+++ b/plugins/devtools/src/components/DefaultDevToolsPage/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DefaultDevToolsPage } from './DefaultDevToolsPage';
diff --git a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
new file mode 100644
index 0000000000..12cd3f7666
--- /dev/null
+++ b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Header, Page, RoutedTabs } from '@backstage/core-components';
+import {
+ attachComponentData,
+ useElementFilter,
+} from '@backstage/core-plugin-api';
+import { TabProps } from '@material-ui/core';
+import { default as React } from 'react';
+
+/** @public */
+export type SubRoute = {
+ path: string;
+ title: string;
+ children: JSX.Element;
+ tabProps?: TabProps;
+};
+
+const dataKey = 'plugin.devtools.devtoolsLayoutRoute';
+
+const Route: (props: SubRoute) => null = () => null;
+attachComponentData(Route, dataKey, true);
+
+// This causes all mount points that are discovered within this route to use the path of the route itself
+attachComponentData(Route, 'core.gatherMountPoints', true);
+
+/** @public */
+export type DevToolsLayoutProps = {
+ children?: React.ReactNode;
+};
+
+/**
+ * DevTools is a compound component, which allows you to define a custom layout
+ *
+ * @example
+ * ```jsx
+ *
+ *
+ *
This is rendered under /example/anything-here route
+ *
+ *
+ * ```
+ * @public
+ */
+export const DevToolsLayout = ({ children }: DevToolsLayoutProps) => {
+ const routes = useElementFilter(children, elements =>
+ elements
+ .selectByComponentData({
+ key: dataKey,
+ withStrictError:
+ 'Child of DevToolsLayout must be an DevToolsLayout.Route',
+ })
+ .getElements()
+ .map(child => child.props),
+ );
+
+ return (
+
+
+
+
+ );
+};
+
+DevToolsLayout.Route = Route;
diff --git a/plugins/devtools/src/components/DevToolsLayout/index.tsx b/plugins/devtools/src/components/DevToolsLayout/index.tsx
new file mode 100644
index 0000000000..0bef371b05
--- /dev/null
+++ b/plugins/devtools/src/components/DevToolsLayout/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type { DevToolsLayoutProps, SubRoute } from './DevToolsLayout';
+export { DevToolsLayout } from './DevToolsLayout';
diff --git a/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx
new file mode 100644
index 0000000000..880efed68c
--- /dev/null
+++ b/plugins/devtools/src/components/DevToolsPage/DevToolsPage.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { useOutlet } from 'react-router-dom';
+import { DefaultDevToolsPage } from '../DefaultDevToolsPage';
+
+export const DevToolsPage = () => {
+ const outlet = useOutlet();
+
+ return <>{outlet || }>;
+};
diff --git a/plugins/devtools/src/components/DevToolsPage/index.ts b/plugins/devtools/src/components/DevToolsPage/index.ts
new file mode 100644
index 0000000000..88d8bc06fa
--- /dev/null
+++ b/plugins/devtools/src/components/DevToolsPage/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DevToolsPage } from './DevToolsPage';
diff --git a/plugins/devtools/src/components/index.ts b/plugins/devtools/src/components/index.ts
new file mode 100644
index 0000000000..9938866bcc
--- /dev/null
+++ b/plugins/devtools/src/components/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './Content';
+export * from './DevToolsLayout';
diff --git a/plugins/devtools/src/hooks/index.ts b/plugins/devtools/src/hooks/index.ts
new file mode 100644
index 0000000000..bb2e83d06f
--- /dev/null
+++ b/plugins/devtools/src/hooks/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { useConfig } from './useConfig';
+export { useExternalDependencies } from './useExternalDependencies';
+export { useInfo } from './useInfo';
diff --git a/plugins/devtools/src/hooks/useConfig.ts b/plugins/devtools/src/hooks/useConfig.ts
new file mode 100644
index 0000000000..6703e7ea59
--- /dev/null
+++ b/plugins/devtools/src/hooks/useConfig.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { devToolsApiRef } from '../api';
+import { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { ConfigInfo } from '@backstage/plugin-devtools-common';
+
+export function useConfig(): {
+ configInfo?: ConfigInfo;
+ loading: boolean;
+ error?: Error;
+} {
+ const api = useApi(devToolsApiRef);
+ const { value, loading, error } = useAsync(() => {
+ return api.getConfig();
+ }, [api]);
+
+ return {
+ configInfo: value,
+ loading,
+ error,
+ };
+}
diff --git a/plugins/devtools/src/hooks/useExternalDependencies.ts b/plugins/devtools/src/hooks/useExternalDependencies.ts
new file mode 100644
index 0000000000..df2f3e193b
--- /dev/null
+++ b/plugins/devtools/src/hooks/useExternalDependencies.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { devToolsApiRef } from '../api';
+import { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { ExternalDependency } from '@backstage/plugin-devtools-common';
+
+export function useExternalDependencies(): {
+ externalDependencies?: ExternalDependency[];
+ loading: boolean;
+ error?: Error;
+} {
+ const api = useApi(devToolsApiRef);
+ const { value, loading, error } = useAsync(() => {
+ return api.getExternalDependencies();
+ }, [api]);
+
+ return {
+ externalDependencies: value,
+ loading,
+ error,
+ };
+}
diff --git a/plugins/devtools/src/hooks/useInfo.ts b/plugins/devtools/src/hooks/useInfo.ts
new file mode 100644
index 0000000000..ad700cb3fe
--- /dev/null
+++ b/plugins/devtools/src/hooks/useInfo.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { devToolsApiRef } from '../api';
+import { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { DevToolsInfo } from '@backstage/plugin-devtools-common';
+
+export function useInfo(): {
+ about?: DevToolsInfo;
+ loading: boolean;
+ error?: Error;
+} {
+ const api = useApi(devToolsApiRef);
+ const { value, loading, error } = useAsync(() => {
+ return api.getInfo();
+ }, [api]);
+
+ return {
+ about: value,
+ loading,
+ error,
+ };
+}
diff --git a/plugins/devtools/src/index.ts b/plugins/devtools/src/index.ts
new file mode 100644
index 0000000000..1077a2518c
--- /dev/null
+++ b/plugins/devtools/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Backstage DevTools frontend plugin with a tab that shows package dependencies and another tab that shows your current configuration.
+ *
+ * @packageDocumentation
+ */
+
+export { devToolsPlugin, DevToolsPage } from './plugin';
+export * from './components';
diff --git a/plugins/devtools/src/plugin.test.ts b/plugins/devtools/src/plugin.test.ts
new file mode 100644
index 0000000000..62f4ae5a61
--- /dev/null
+++ b/plugins/devtools/src/plugin.test.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { devToolsPlugin } from './plugin';
+
+describe('devtools', () => {
+ it('should export plugin', () => {
+ expect(devToolsPlugin).toBeDefined();
+ });
+});
diff --git a/plugins/devtools/src/plugin.ts b/plugins/devtools/src/plugin.ts
new file mode 100644
index 0000000000..df2aca3447
--- /dev/null
+++ b/plugins/devtools/src/plugin.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createApiFactory,
+ createPlugin,
+ createRoutableExtension,
+ discoveryApiRef,
+ identityApiRef,
+} from '@backstage/core-plugin-api';
+import { devToolsApiRef, DevToolsClient } from './api';
+
+import { rootRouteRef } from './routes';
+
+/** @public */
+export const devToolsPlugin = createPlugin({
+ id: 'devtools',
+ apis: [
+ createApiFactory({
+ api: devToolsApiRef,
+ deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
+ factory: ({ discoveryApi, identityApi }) =>
+ new DevToolsClient({ discoveryApi, identityApi }),
+ }),
+ ],
+ routes: {
+ root: rootRouteRef,
+ },
+});
+
+/** @public */
+export const DevToolsPage = devToolsPlugin.provide(
+ createRoutableExtension({
+ name: 'DevToolsPage',
+ component: () =>
+ import('./components/DevToolsPage').then(m => m.DevToolsPage),
+ mountPoint: rootRouteRef,
+ }),
+);
diff --git a/plugins/devtools/src/routes.ts b/plugins/devtools/src/routes.ts
new file mode 100644
index 0000000000..7825f928be
--- /dev/null
+++ b/plugins/devtools/src/routes.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createRouteRef } from '@backstage/core-plugin-api';
+
+export const rootRouteRef = createRouteRef({
+ id: 'devtools',
+});
diff --git a/plugins/devtools/src/setupTests.ts b/plugins/devtools/src/setupTests.ts
new file mode 100644
index 0000000000..7a459ed24e
--- /dev/null
+++ b/plugins/devtools/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '@testing-library/jest-dom';
diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md
index 5ede228812..bfb0eac993 100644
--- a/plugins/dynatrace/CHANGELOG.md
+++ b/plugins/dynatrace/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-dynatrace
+## 5.0.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 5.0.0-next.1
### Patch Changes
diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json
index b7c8244753..ab241ec0ad 100644
--- a/plugins/dynatrace/package.json
+++ b/plugins/dynatrace/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-dynatrace",
- "version": "5.0.0-next.1",
+ "version": "5.0.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/entity-feedback/CHANGELOG.md b/plugins/entity-feedback/CHANGELOG.md
index 3f4575ca8a..d32012518e 100644
--- a/plugins/entity-feedback/CHANGELOG.md
+++ b/plugins/entity-feedback/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-entity-feedback
+## 0.2.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.1-next.1
### Patch Changes
diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json
index 0c15a0f3c8..0b90caea9d 100644
--- a/plugins/entity-feedback/package.json
+++ b/plugins/entity-feedback/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-entity-feedback",
- "version": "0.2.1-next.1",
+ "version": "0.2.1-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md
index 9b63d46c5b..6d423d04bf 100644
--- a/plugins/entity-validation/CHANGELOG.md
+++ b/plugins/entity-validation/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-entity-validation
+## 0.1.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.3-next.1
### Patch Changes
diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json
index 674c5c6a56..950687a884 100644
--- a/plugins/entity-validation/package.json
+++ b/plugins/entity-validation/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-entity-validation",
- "version": "0.1.3-next.1",
+ "version": "0.1.3-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md
index 9b076afbd3..241463ac5c 100644
--- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md
+++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-events-backend-module-aws-sqs
+## 0.2.0-next.2
+
+### Minor Changes
+
+- 2c5661f3899: Allow endpoint configuration for sqs, enabling use of localstack for testing.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.1.7-next.1
### Patch Changes
diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json
index 0125d5e5b4..f9b3fd071b 100644
--- a/plugins/events-backend-module-aws-sqs/package.json
+++ b/plugins/events-backend-module-aws-sqs/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-aws-sqs",
- "version": "0.1.7-next.1",
+ "version": "0.2.0-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -35,7 +35,7 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
- "@aws-sdk/client-sqs": "^3.208.0",
+ "@aws-sdk/client-sqs": "^3.310.0",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
@@ -46,7 +46,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@aws-sdk/types": "^3.208.0",
+ "@aws-sdk/types": "^3.310.0",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts
index e649e2ee83..4f4e6ea55d 100644
--- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts
+++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts
@@ -68,7 +68,11 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher {
VisibilityTimeout: config.visibilityTimeout?.as('seconds'),
WaitTimeSeconds: config.pollingWaitTime.as('seconds'),
};
- this.sqs = new SQSClient({ region: config.region });
+
+ this.sqs = new SQSClient({
+ region: config.region,
+ endpoint: config.endpoint,
+ });
this.queueUrl = config.queueUrl;
this.taskTimeoutSeconds = config.timeout.as('seconds');
diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts
index 1fa9df4ce3..6d7a08d7a7 100644
--- a/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts
+++ b/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts
@@ -79,6 +79,7 @@ describe('readConfig', () => {
url: 'https://fake1.queue.url',
visibilityTimeout: { minutes: 5 },
waitTime: { seconds: 10 },
+ endpoint: 'https://fake.endpoint.url',
},
timeout: { minutes: 5 },
waitTimeAfterEmptyReceive: { seconds: 30 },
@@ -97,6 +98,7 @@ describe('readConfig', () => {
expect(publisherConfigs[0].topic).toEqual('fake1');
expect(publisherConfigs[0].region).toEqual('eu-west-1');
expect(publisherConfigs[0].queueUrl).toEqual('https://fake1.queue.url');
+ expect(publisherConfigs[0].endpoint).toEqual('https://fake.endpoint.url');
expect(publisherConfigs[0].pollingWaitTime.as('seconds')).toBe(10);
expect(publisherConfigs[0].timeout.as('seconds')).toBe(300);
expect(publisherConfigs[0].waitTimeAfterEmptyReceive.as('seconds')).toBe(
diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/config.ts b/plugins/events-backend-module-aws-sqs/src/publisher/config.ts
index c6b74ea8d3..eeb650ff6c 100644
--- a/plugins/events-backend-module-aws-sqs/src/publisher/config.ts
+++ b/plugins/events-backend-module-aws-sqs/src/publisher/config.ts
@@ -31,6 +31,7 @@ export interface AwsSqsEventSourceConfig {
topic: string;
visibilityTimeout?: Duration;
waitTimeAfterEmptyReceive: Duration;
+ endpoint?: string;
}
// TODO(pjungermann): validation could be improved similar to `convertToHumanDuration` at @backstage/backend-tasks
@@ -74,6 +75,7 @@ export function readConfig(config: Config): AwsSqsEventSourceConfig[] {
}
const queueUrl = topicConfig.getString('queue.url');
const region = topicConfig.getString('queue.region');
+ const endpoint = topicConfig.getOptionalString('queue.endpoint');
const visibilityTimeout = readOptionalDuration(
topicConfig,
'queue.visibilityTimeout',
@@ -106,6 +108,7 @@ export function readConfig(config: Config): AwsSqsEventSourceConfig[] {
return {
pollingWaitTime,
queueUrl,
+ endpoint,
region,
timeout,
topic,
diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md
index 87cc779cde..c12164fa62 100644
--- a/plugins/example-todo-list/CHANGELOG.md
+++ b/plugins/example-todo-list/CHANGELOG.md
@@ -1,5 +1,14 @@
# @internal/plugin-todo-list
+## 1.0.13-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 1.0.13-next.0
### Patch Changes
diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json
index fcd193bd5f..4ee26dea2c 100644
--- a/plugins/example-todo-list/package.json
+++ b/plugins/example-todo-list/package.json
@@ -1,6 +1,6 @@
{
"name": "@internal/plugin-todo-list",
- "version": "1.0.13-next.0",
+ "version": "1.0.13-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md
index 3ad9bdb917..36d04b4231 100644
--- a/plugins/explore/CHANGELOG.md
+++ b/plugins/explore/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-explore
+## 0.4.3-next.2
+
+### Patch Changes
+
+- 4851581deb6: Display the title of the entity on the explore card if present, otherwise stick to the name
+- a6025e25d99: Updated the example code in the "Customization" section of the README to make the imports match the components used.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/plugin-explore-react@0.0.28
+ - @backstage/core-plugin-api@1.5.1
+
## 0.4.3-next.1
### Patch Changes
diff --git a/plugins/explore/README.md b/plugins/explore/README.md
index a1cb273aab..24e6496cc3 100644
--- a/plugins/explore/README.md
+++ b/plugins/explore/README.md
@@ -104,11 +104,11 @@ Create a custom explore page in
```tsx
import {
- DomainExplorerContent,
+ CatalogKindExploreContent,
ExploreLayout,
} from '@backstage/plugin-explore';
import React from 'react';
-import { InnserSourceExplorerContent } from './InnserSourceExplorerContent';
+import { InnerSourceExploreContent } from './InnerSourceExploreContent';
export const ExplorePage = () => {
return (
@@ -123,7 +123,7 @@ export const ExplorePage = () => {
-
+
);
diff --git a/plugins/explore/package.json b/plugins/explore/package.json
index bbe114c1f0..21c8fcc58b 100644
--- a/plugins/explore/package.json
+++ b/plugins/explore/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-explore",
"description": "A Backstage plugin for building an exploration page of your software ecosystem",
- "version": "0.4.3-next.1",
+ "version": "0.4.3-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx
index df9b84820a..b66e30c652 100644
--- a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx
+++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx
@@ -17,6 +17,7 @@
import { DomainEntity } from '@backstage/catalog-model';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
+import { screen } from '@testing-library/react';
import React from 'react';
import { DomainCard } from './DomainCard';
@@ -51,4 +52,33 @@ describe('', () => {
'/catalog/default/domain/artists',
);
});
+
+ it('renders a domain card with custom title', async () => {
+ const entity: DomainEntity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Domain',
+ metadata: {
+ name: 'artists',
+ title: 'my Custom Title',
+ },
+ spec: {
+ owner: 'guest',
+ },
+ };
+ const { getByText } = await renderInTestApp(
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+
+ expect(getByText('Explore').parentElement).toHaveAttribute(
+ 'href',
+ '/catalog/default/domain/artists',
+ );
+ expect(getByText('my Custom Title')).toBeInTheDocument();
+ expect(screen.queryByText('artists')).not.toBeInTheDocument();
+ });
});
diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx
index 0d37e3e273..c70df46b1a 100644
--- a/plugins/explore/src/components/DomainCard/DomainCard.tsx
+++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx
@@ -53,7 +53,10 @@ export const DomainCard = (props: { entity: DomainEntity }) => {
return (
-
+
{entity.metadata.tags?.length ? (
diff --git a/plugins/explore/src/components/EntityCard/EntityCard.test.tsx b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx
index 4b006e347f..cebd5af3d6 100644
--- a/plugins/explore/src/components/EntityCard/EntityCard.test.tsx
+++ b/plugins/explore/src/components/EntityCard/EntityCard.test.tsx
@@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
+import { screen } from '@testing-library/react';
import { EntityCard } from './EntityCard';
describe('', () => {
@@ -51,4 +52,29 @@ describe('', () => {
'/catalog/default/domain/artists',
);
});
+
+ it('renders an entity card with display title', async () => {
+ const entity: Entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Domain',
+ metadata: {
+ name: 'artists',
+ title: 'New Title',
+ },
+ spec: {
+ owner: 'guest',
+ },
+ };
+ const { getByText } = await renderInTestApp(
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+
+ expect(getByText('New Title')).toBeInTheDocument();
+ expect(screen.queryByText('artists')).not.toBeInTheDocument();
+ });
});
diff --git a/plugins/explore/src/components/EntityCard/EntityCard.tsx b/plugins/explore/src/components/EntityCard/EntityCard.tsx
index 0a5908df38..9589d4721e 100644
--- a/plugins/explore/src/components/EntityCard/EntityCard.tsx
+++ b/plugins/explore/src/components/EntityCard/EntityCard.tsx
@@ -53,7 +53,10 @@ export const EntityCard = (props: { entity: Entity }) => {
return (
-
+
{entity.metadata.tags?.length ? (
diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md
index 27c6a1b62e..9822bb684e 100644
--- a/plugins/firehydrant/CHANGELOG.md
+++ b/plugins/firehydrant/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-firehydrant
+## 0.2.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.2-next.1
### Patch Changes
diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json
index d5e136229c..260b3ad47f 100644
--- a/plugins/firehydrant/package.json
+++ b/plugins/firehydrant/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-firehydrant",
"description": "A Backstage plugin that integrates towards FireHydrant",
- "version": "0.2.2-next.1",
+ "version": "0.2.2-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md
index 0399ad0ce2..ea411c0558 100644
--- a/plugins/fossa/CHANGELOG.md
+++ b/plugins/fossa/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-fossa
+## 0.2.50-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.50-next.1
### Patch Changes
diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json
index 985202a403..a1b9e48f3b 100644
--- a/plugins/fossa/package.json
+++ b/plugins/fossa/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-fossa",
"description": "A Backstage plugin that integrates towards FOSSA",
- "version": "0.2.50-next.1",
+ "version": "0.2.50-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md
index 1c23885154..b211ebd95a 100644
--- a/plugins/gcalendar/CHANGELOG.md
+++ b/plugins/gcalendar/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-gcalendar
+## 0.3.14-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.14-next.0
### Patch Changes
diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json
index 0cc77cabad..10375e2e3f 100644
--- a/plugins/gcalendar/package.json
+++ b/plugins/gcalendar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gcalendar",
- "version": "0.3.14-next.0",
+ "version": "0.3.14-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/gcalendar/src/hooks/useSignIn.ts b/plugins/gcalendar/src/hooks/useSignIn.ts
index 351707b54e..3dbc778cf1 100644
--- a/plugins/gcalendar/src/hooks/useSignIn.ts
+++ b/plugins/gcalendar/src/hooks/useSignIn.ts
@@ -25,7 +25,10 @@ export const useSignIn = () => {
const signIn = useCallback(
async (optional = false) => {
const token = await authApi.getAccessToken(
- 'https://www.googleapis.com/auth/calendar.readonly',
+ [
+ 'https://www.googleapis.com/auth/calendar.readonly',
+ 'https://www.googleapis.com/auth/userinfo.email',
+ ],
{
optional,
instantPopup: !optional,
diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md
index 7a5ea25c0d..8254e86dbc 100644
--- a/plugins/gcp-projects/CHANGELOG.md
+++ b/plugins/gcp-projects/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-gcp-projects
+## 0.3.37-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.37-next.0
### Patch Changes
diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json
index 039bcd094c..9857a6ccf1 100644
--- a/plugins/gcp-projects/package.json
+++ b/plugins/gcp-projects/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-gcp-projects",
"description": "A Backstage plugin that helps you manage projects in GCP",
- "version": "0.3.37-next.0",
+ "version": "0.3.37-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md
index 976585c656..7899cff4fe 100644
--- a/plugins/git-release-manager/CHANGELOG.md
+++ b/plugins/git-release-manager/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-git-release-manager
+## 0.3.31-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.31-next.1
### Patch Changes
diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json
index 7bc4df059e..a73c35aead 100644
--- a/plugins/git-release-manager/package.json
+++ b/plugins/git-release-manager/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-git-release-manager",
"description": "A Backstage plugin that helps you manage releases in git",
- "version": "0.3.31-next.1",
+ "version": "0.3.31-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md
index 09b205c9f1..8b64c47ac8 100644
--- a/plugins/github-actions/CHANGELOG.md
+++ b/plugins/github-actions/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-github-actions
+## 0.5.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.5.18-next.1
### Patch Changes
diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json
index 25fac2b774..a2c25741be 100644
--- a/plugins/github-actions/package.json
+++ b/plugins/github-actions/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-github-actions",
"description": "A Backstage plugin that integrates towards GitHub Actions",
- "version": "0.5.18-next.1",
+ "version": "0.5.18-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md
index 41475febd3..ba1d67aece 100644
--- a/plugins/github-deployments/CHANGELOG.md
+++ b/plugins/github-deployments/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-github-deployments
+## 0.1.49-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.49-next.1
### Patch Changes
diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json
index dcb4156f9d..c9df936591 100644
--- a/plugins/github-deployments/package.json
+++ b/plugins/github-deployments/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-github-deployments",
"description": "A Backstage plugin that integrates towards GitHub Deployments",
- "version": "0.1.49-next.1",
+ "version": "0.1.49-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md
index d1e2103800..ee2e47570d 100644
--- a/plugins/github-issues/CHANGELOG.md
+++ b/plugins/github-issues/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-github-issues
+## 0.2.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.7-next.1
### Patch Changes
diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json
index ed982410e9..8a12aeb2de 100644
--- a/plugins/github-issues/package.json
+++ b/plugins/github-issues/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-issues",
- "version": "0.2.7-next.1",
+ "version": "0.2.7-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md
index 7421c6e015..53bcb9b570 100644
--- a/plugins/github-pull-requests-board/CHANGELOG.md
+++ b/plugins/github-pull-requests-board/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-github-pull-requests-board
+## 0.1.12-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.12-next.1
### Patch Changes
diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json
index e805cd99ce..824d265abe 100644
--- a/plugins/github-pull-requests-board/package.json
+++ b/plugins/github-pull-requests-board/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-github-pull-requests-board",
"description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team",
- "version": "0.1.12-next.1",
+ "version": "0.1.12-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md
index 99122116fb..c5eba8def3 100644
--- a/plugins/gitops-profiles/CHANGELOG.md
+++ b/plugins/gitops-profiles/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-gitops-profiles
+## 0.3.36-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.36-next.0
### Patch Changes
diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json
index 2bb6f145a4..e721682976 100644
--- a/plugins/gitops-profiles/package.json
+++ b/plugins/gitops-profiles/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-gitops-profiles",
"description": "A Backstage plugin that helps you manage GitOps profiles",
- "version": "0.3.36-next.0",
+ "version": "0.3.36-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md
index 14a0149ca5..9eb9964c96 100644
--- a/plugins/gocd/CHANGELOG.md
+++ b/plugins/gocd/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-gocd
+## 0.1.24-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.24-next.1
### Patch Changes
diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json
index 2e41e2c217..c8fca38e23 100644
--- a/plugins/gocd/package.json
+++ b/plugins/gocd/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-gocd",
"description": "A Backstage plugin that integrates towards GoCD",
- "version": "0.1.24-next.1",
+ "version": "0.1.24-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md
index e831845d49..3d49c1b29f 100644
--- a/plugins/graphiql/CHANGELOG.md
+++ b/plugins/graphiql/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-graphiql
+## 0.2.50-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.50-next.0
### Patch Changes
diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json
index 08fc2a6ec0..219551e6e7 100644
--- a/plugins/graphiql/package.json
+++ b/plugins/graphiql/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphiql",
"description": "Backstage plugin for browsing GraphQL APIs",
- "version": "0.2.50-next.0",
+ "version": "0.2.50-next.1",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md
index 77586073af..c47af7b006 100644
--- a/plugins/graphql-voyager/CHANGELOG.md
+++ b/plugins/graphql-voyager/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-graphql-voyager
+## 0.1.3-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.3-next.0
### Patch Changes
diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json
index d356dee1c5..c459e4fbf8 100644
--- a/plugins/graphql-voyager/package.json
+++ b/plugins/graphql-voyager/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphql-voyager",
"description": "Backstage plugin for GraphQL Voyager",
- "version": "0.1.3-next.0",
+ "version": "0.1.3-next.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md
index 868b1a6c3b..3996c77103 100644
--- a/plugins/home/CHANGELOG.md
+++ b/plugins/home/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-home
+## 0.5.2-next.2
+
+### Patch Changes
+
+- acca8966465: Remove object-hash dependency
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.5.2-next.1
### Patch Changes
diff --git a/plugins/home/package.json b/plugins/home/package.json
index ef9267234b..12e55d80cd 100644
--- a/plugins/home/package.json
+++ b/plugins/home/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-home",
"description": "A Backstage plugin that helps you build a home page",
- "version": "0.5.2-next.1",
+ "version": "0.5.2-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -48,7 +48,6 @@
"@rjsf/validator-ajv8": "5.6.0",
"@types/react": "^16.13.1 || ^17.0.0",
"lodash": "^4.17.21",
- "object-hash": "^3.0.0",
"react-grid-layout": "^1.3.4",
"react-resizable": "^3.0.4",
"react-use": "^17.2.4",
diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx
index 232fbd12c2..6af9044cbd 100644
--- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx
+++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx
@@ -46,7 +46,6 @@ import { CardConfig } from '../../extensions';
// eslint-disable-next-line new-cap
const ResponsiveGrid = WidthProvider(Responsive);
-const hash = require('object-hash');
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -336,14 +335,14 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => {
...widget.component.props,
...(w.settings ?? {}),
};
- const propsHash = hash(widgetProps, {});
+
return (
-
+
{editMode && (
{
/>,
);
- expect(rendered.getByText('09:10')).toBeInTheDocument();
+ expect(rendered.getByText('09:10')).toHaveAttribute('datetime', '09:10');
});
});
diff --git a/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx b/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx
index a45a51e0ad..394af1a27b 100644
--- a/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx
+++ b/plugins/home/src/homePageComponents/HeaderWorldClock/HeaderWorldClock.tsx
@@ -23,8 +23,9 @@ const timeFormat: Intl.DateTimeFormatOptions = {
};
type TimeObj = {
- time: string;
label: string;
+ value: string;
+ dateTime: string;
};
/** @public */
@@ -65,8 +66,14 @@ function getTimes(
label = 'GMT';
}
- const time = d.toLocaleTimeString(lang, options);
- clocks.push({ time, label });
+ const value = d.toLocaleTimeString(lang, options);
+ const dateTime = d.toLocaleTimeString(lang, {
+ timeZone: options.timeZone,
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+ clocks.push({ label, value, dateTime });
}
return clocks;
@@ -129,9 +136,9 @@ export const HeaderWorldClock = (props: {
<>
{clocks.map(clock => (
{clock.value}}
/>
))}
>
diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md
index 0f5b044bfb..a1fb6e375d 100644
--- a/plugins/ilert/CHANGELOG.md
+++ b/plugins/ilert/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-ilert
+## 0.2.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.7-next.1
### Patch Changes
diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json
index 17e94d54e5..0e08159481 100644
--- a/plugins/ilert/package.json
+++ b/plugins/ilert/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-ilert",
"description": "A Backstage plugin that integrates towards iLert",
- "version": "0.2.7-next.1",
+ "version": "0.2.7-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json
index 4f58776df7..07624593c3 100644
--- a/plugins/jenkins-backend/package.json
+++ b/plugins/jenkins-backend/package.json
@@ -36,6 +36,7 @@
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"jenkins": "^1.0.0",
+ "node-fetch": "^2.6.7",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts
index c89a3866c7..8e80de34a3 100644
--- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts
+++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts
@@ -19,9 +19,10 @@ import jenkins from 'jenkins';
import { JenkinsInfo } from './jenkinsInfoProvider';
import { JenkinsBuild, JenkinsProject } from '../types';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
-import { NotAllowedError } from '@backstage/errors';
+import fetch, { Response } from 'node-fetch';
jest.mock('jenkins');
+jest.mock('node-fetch');
const mockedJenkinsClient = {
job: {
get: jest.fn(),
@@ -54,6 +55,11 @@ const fakePermissionApi = {
describe('JenkinsApi', () => {
const jenkinsApi = new JenkinsApiImpl(fakePermissionApi);
+ const mockFetch = fetch as jest.MockedFunction;
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
describe('getProjects', () => {
const project: JenkinsProject = {
@@ -689,55 +695,63 @@ describe('JenkinsApi', () => {
buildNumber,
);
});
- it('buildProject', async () => {
- await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef);
+ it('getBuildUrl', async () => {
+ const jenkinsApiProto = Object.getPrototypeOf(jenkinsApi);
+ const buildUrl = jenkinsApiProto.getBuildUrl(
+ jenkinsInfo,
+ jobFullName,
+ buildNumber,
+ );
+ expect(buildUrl).toEqual(
+ 'https://jenkins.example.com/job/example-jobName/job/foo/19',
+ );
- expect(mockedJenkins).toHaveBeenCalledWith({
- baseUrl: jenkinsInfo.baseUrl,
- headers: jenkinsInfo.headers,
- promisify: true,
- });
- expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
+ const buildUrlTriple = jenkinsApiProto.getBuildUrl(
+ jenkinsInfo,
+ 'example-jobName/foo/bar',
+ buildNumber,
+ );
+ expect(buildUrlTriple).toEqual(
+ 'https://jenkins.example.com/job/example-jobName/job/foo/job/bar/19',
+ );
});
-
- it('buildProject should fail if it does not have required permissions', async () => {
- fakePermissionApi.authorize.mockResolvedValueOnce([
- {
- result: AuthorizeResult.DENY,
- },
- ]);
-
- await expect(() =>
- jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef),
- ).rejects.toThrow(NotAllowedError);
- });
-
- it('buildProject should succeed if it have required permissions', async () => {
- fakePermissionApi.authorize.mockResolvedValueOnce([
- {
- result: AuthorizeResult.ALLOW,
- },
- ]);
-
- await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef);
- expect(mockedJenkins).toHaveBeenCalledWith({
- baseUrl: jenkinsInfo.baseUrl,
- headers: jenkinsInfo.headers,
- promisify: true,
+ describe('rebuildProject', () => {
+ it('successfully rebuilds', async () => {
+ mockFetch.mockResolvedValueOnce({ status: 200 } as Response);
+ const status = await jenkinsApi.rebuildProject(
+ jenkinsInfo,
+ jobFullName,
+ buildNumber,
+ resourceRef,
+ );
+ expect(status).toEqual(200);
});
- expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
- });
-
- it('buildProject with crumbIssuer option', async () => {
- const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true };
- await jenkinsApi.buildProject(info, jobFullName, resourceRef);
-
- expect(mockedJenkins).toHaveBeenCalledWith({
- baseUrl: jenkinsInfo.baseUrl,
- headers: jenkinsInfo.headers,
- promisify: true,
- crumbIssuer: true,
+ it('fails to rebuild', async () => {
+ mockFetch.mockResolvedValueOnce({ status: 401 } as Response);
+ const status = await jenkinsApi.rebuildProject(
+ jenkinsInfo,
+ jobFullName,
+ buildNumber,
+ resourceRef,
+ );
+ expect(status).toEqual(401);
+ });
+
+ it('should fail if it does not have required permissions', async () => {
+ fakePermissionApi.authorize.mockResolvedValueOnce([
+ {
+ result: AuthorizeResult.DENY,
+ },
+ ]);
+
+ mockFetch.mockResolvedValueOnce({ status: 200 } as Response);
+ const status = await jenkinsApi.rebuildProject(
+ jenkinsInfo,
+ jobFullName,
+ buildNumber,
+ resourceRef,
+ );
+ expect(status).toEqual(401);
});
- expect(mockedJenkinsClient.job.build).toHaveBeenCalledWith(jobFullName);
});
});
diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts
index e14437fa68..85271462eb 100644
--- a/plugins/jenkins-backend/src/service/jenkinsApi.ts
+++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts
@@ -28,7 +28,7 @@ import {
PermissionEvaluator,
} from '@backstage/plugin-permission-common';
import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common';
-import { NotAllowedError } from '@backstage/errors';
+import fetch, { HeaderInit } from 'node-fetch';
export class JenkinsApiImpl {
private static readonly lastBuildTreeSpec = `lastBuild[
@@ -144,14 +144,13 @@ export class JenkinsApiImpl {
* Trigger a build of a project
* @see ../../../jenkins/src/api/JenkinsApi.ts#retry
*/
- async buildProject(
+ async rebuildProject(
jenkinsInfo: JenkinsInfo,
jobFullName: string,
+ buildNumber: number,
resourceRef: string,
options?: { token?: string },
- ) {
- const client = await JenkinsApiImpl.getClient(jenkinsInfo);
-
+ ): Promise {
if (this.permissionApi) {
const response = await this.permissionApi.authorize(
[{ permission: jenkinsExecutePermission, resourceRef }],
@@ -160,16 +159,19 @@ export class JenkinsApiImpl {
// permission api returns always at least one item, we need to check only one result since we do not expect any additional results
const { result } = response[0];
if (result === AuthorizeResult.DENY) {
- throw new NotAllowedError();
+ return 401;
}
}
- // looks like the current SDK only supports triggering a new build
- // can't see any support for replay (re-running the specific build with the same SCM info)
+ const buildUrl = this.getBuildUrl(jenkinsInfo, jobFullName, buildNumber);
- // Note Jenkins itself has concepts of rebuild and replay on a job.
- // The latter should be possible to trigger with a POST to /replay/rebuild
- await client.job.build(jobFullName);
+ // the current SDK only supports triggering a new build
+ // replay the job by triggering request directly from Jenkins api
+ const response = await fetch(`${buildUrl}/replay/rebuild`, {
+ method: 'post',
+ headers: jenkinsInfo.headers as HeaderInit,
+ });
+ return response.status;
}
// private helper methods
@@ -318,4 +320,13 @@ export class JenkinsApiImpl {
})
.pop();
}
+
+ private getBuildUrl(
+ jenkinsInfo: JenkinsInfo,
+ jobFullName: string,
+ buildId: number,
+ ): string {
+ const jobs = jobFullName.split('/');
+ return `${jenkinsInfo.baseUrl}/job/${jobs.join('/job/')}/${buildId}`;
+ }
}
diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts
index df4c3b5a11..484bfa273f 100644
--- a/plugins/jenkins-backend/src/service/router.ts
+++ b/plugins/jenkins-backend/src/service/router.ts
@@ -145,9 +145,10 @@ export async function createRouter(
);
router.post(
- '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber::rebuild',
+ '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber',
async (request, response) => {
- const { namespace, kind, name, jobFullName } = request.params;
+ const { namespace, kind, name, jobFullName, buildNumber } =
+ request.params;
const token = getBearerTokenFromAuthorizationHeader(
request.header('authorization'),
);
@@ -162,10 +163,16 @@ export async function createRouter(
});
const resourceRef = stringifyEntityRef({ kind, namespace, name });
- await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef, {
- token,
- });
- response.json({});
+ const status = await jenkinsApi.rebuildProject(
+ jenkinsInfo,
+ jobFullName,
+ parseInt(buildNumber, 10),
+ resourceRef,
+ {
+ token,
+ },
+ );
+ response.json({}).status(status);
},
);
router.use(errorHandler());
diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md
index 6b900dcb18..9d617e1d39 100644
--- a/plugins/jenkins/CHANGELOG.md
+++ b/plugins/jenkins/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-jenkins
+## 0.7.17-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.7.17-next.1
### Patch Changes
diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json
index e57c1c77f2..6d8d50d26b 100644
--- a/plugins/jenkins/package.json
+++ b/plugins/jenkins/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-jenkins",
"description": "A Backstage plugin that integrates towards Jenkins",
- "version": "0.7.17-next.1",
+ "version": "0.7.17-next.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/jenkins/src/assets/folder-results.png b/plugins/jenkins/src/assets/folder-results.png
index 2c14d5d927..340b03549d 100644
Binary files a/plugins/jenkins/src/assets/folder-results.png and b/plugins/jenkins/src/assets/folder-results.png differ
diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx
index 1bb6dbaefc..f2d108eaf7 100644
--- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx
+++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx
@@ -18,6 +18,7 @@ import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
import { Box, IconButton, Tooltip, Typography } from '@material-ui/core';
import RetryIcon from '@material-ui/icons/Replay';
+import VisibilityIcon from '@material-ui/icons/Visibility';
import { default as React, useState } from 'react';
import { Project } from '../../../../api/JenkinsApi';
import JenkinsLogo from '../../../../assets/JenkinsLogo.svg';
@@ -203,16 +204,23 @@ const generatedColumns: TableColumn[] = [
};
return (
-
- <>
- {isLoadingRebuild && }
- {!isLoadingRebuild && (
+