diff --git a/.changeset/angry-dolphins-camp.md b/.changeset/angry-dolphins-camp.md new file mode 100644 index 0000000000..3c598f4a66 --- /dev/null +++ b/.changeset/angry-dolphins-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Add access restrictions to the JWKS external access method config schema diff --git a/.changeset/breezy-cats-kiss.md b/.changeset/breezy-cats-kiss.md new file mode 100644 index 0000000000..900016c1e8 --- /dev/null +++ b/.changeset/breezy-cats-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Fixing issue with extension blueprints `inputs` merging. diff --git a/.changeset/breezy-rings-fly.md b/.changeset/breezy-rings-fly.md new file mode 100644 index 0000000000..c3cc1c143d --- /dev/null +++ b/.changeset/breezy-rings-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes diff --git a/.changeset/bright-donkeys-buy.md b/.changeset/bright-donkeys-buy.md new file mode 100644 index 0000000000..ded16ccc2c --- /dev/null +++ b/.changeset/bright-donkeys-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. diff --git a/.changeset/calm-crabs-drop.md b/.changeset/calm-crabs-drop.md new file mode 100644 index 0000000000..09bed7c091 --- /dev/null +++ b/.changeset/calm-crabs-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. diff --git a/.changeset/chilly-days-peel.md b/.changeset/chilly-days-peel.md new file mode 100644 index 0000000000..d82344d364 --- /dev/null +++ b/.changeset/chilly-days-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. diff --git a/.changeset/clever-pans-brake.md b/.changeset/clever-pans-brake.md new file mode 100644 index 0000000000..1f49c7a842 --- /dev/null +++ b/.changeset/clever-pans-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Added new APIs for testing extensions diff --git a/.changeset/curvy-pillows-joke.md b/.changeset/curvy-pillows-joke.md new file mode 100644 index 0000000000..25966a51c0 --- /dev/null +++ b/.changeset/curvy-pillows-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. diff --git a/.changeset/cyan-shrimps-push.md b/.changeset/cyan-shrimps-push.md new file mode 100644 index 0000000000..cb326b4da3 --- /dev/null +++ b/.changeset/cyan-shrimps-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. diff --git a/.changeset/eighty-jokes-deny.md b/.changeset/eighty-jokes-deny.md new file mode 100644 index 0000000000..58944252f4 --- /dev/null +++ b/.changeset/eighty-jokes-deny.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Add support for status filtering in scaffolder tasks endpoint diff --git a/.changeset/fair-hairs-mix.md b/.changeset/fair-hairs-mix.md new file mode 100644 index 0000000000..6c06c7bf72 --- /dev/null +++ b/.changeset/fair-hairs-mix.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-permission-common': patch +--- + +Add the MetadataResponse type from @backstage/plugin-permission-node, since this +type might be used in frontend code. diff --git a/.changeset/fair-pumas-hang.md b/.changeset/fair-pumas-hang.md new file mode 100644 index 0000000000..156779b76a --- /dev/null +++ b/.changeset/fair-pumas-hang.md @@ -0,0 +1,43 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend-module-gerrit': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-search-backend-module-explore': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-notifications-node': patch +'@backstage/plugin-permission-backend': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-kubernetes-node': patch +'@backstage/plugin-signals-backend': patch +'@backstage/config-loader': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/create-app': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/cli': patch +--- + +Make sure node-fetch is version 2.7.0 or greater diff --git a/.changeset/fast-bulldogs-relax.md b/.changeset/fast-bulldogs-relax.md new file mode 100644 index 0000000000..65895345e9 --- /dev/null +++ b/.changeset/fast-bulldogs-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Added autocompletion support for resource `branches` diff --git a/.changeset/flat-papayas-push.md b/.changeset/flat-papayas-push.md new file mode 100644 index 0000000000..f9e20613fd --- /dev/null +++ b/.changeset/flat-papayas-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-azure': patch +--- + +Added examples for publish:azure action and updated its test cases diff --git a/.changeset/friendly-cherries-applaud.md b/.changeset/friendly-cherries-applaud.md new file mode 100644 index 0000000000..c0160858f2 --- /dev/null +++ b/.changeset/friendly-cherries-applaud.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +The MetadataResponse type has been moved to @backstage/plugin-permission-common +to match the recent move of MetadataResponseSerializedRule, and should be +imported from there going forward. To avoid an immediate breaking change, this +type is still re-exported from this package, but is marked as deprecated and +will be removed in a future release. diff --git a/.changeset/friendly-chicken-cry.md b/.changeset/friendly-chicken-cry.md new file mode 100644 index 0000000000..3eadca8777 --- /dev/null +++ b/.changeset/friendly-chicken-cry.md @@ -0,0 +1,8 @@ +--- +'@backstage/frontend-test-utils': patch +'@backstage/frontend-app-api': patch +'@backstage/core-compat-api': patch +'@backstage/plugin-app-visualizer': patch +--- + +Updated usage of `useRouteRef`, which can now always return `undefined`. diff --git a/.changeset/funny-bears-sort.md b/.changeset/funny-bears-sort.md new file mode 100644 index 0000000000..a3dad0f489 --- /dev/null +++ b/.changeset/funny-bears-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Switched the `process` polyfill to use `require.resolve` for greater compatability. diff --git a/.changeset/funny-wolves-learn.md b/.changeset/funny-wolves-learn.md new file mode 100644 index 0000000000..2010ccd68b --- /dev/null +++ b/.changeset/funny-wolves-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:issues:create examples diff --git a/.changeset/healthy-fireants-jump.md b/.changeset/healthy-fireants-jump.md new file mode 100644 index 0000000000..7a11015b80 --- /dev/null +++ b/.changeset/healthy-fireants-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Allow for more granular control of TechDocsReaderPage styling. Theme overrides can now be provided to TechDocs without affecting the theme in other areas of Backstage. diff --git a/.changeset/healthy-timers-divide.md b/.changeset/healthy-timers-divide.md new file mode 100644 index 0000000000..0b9d700723 --- /dev/null +++ b/.changeset/healthy-timers-divide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search-common': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +Fix package metadata diff --git a/.changeset/hip-fishes-guess.md b/.changeset/hip-fishes-guess.md new file mode 100644 index 0000000000..909448c673 --- /dev/null +++ b/.changeset/hip-fishes-guess.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-defaults': patch +--- + +The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. diff --git a/.changeset/itchy-experts-tie.md b/.changeset/itchy-experts-tie.md new file mode 100644 index 0000000000..c5bf99ecaf --- /dev/null +++ b/.changeset/itchy-experts-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for github:repo:create action and improved test cases diff --git a/.changeset/khaki-lamps-peel.md b/.changeset/khaki-lamps-peel.md new file mode 100644 index 0000000000..82a2cbc9de --- /dev/null +++ b/.changeset/khaki-lamps-peel.md @@ -0,0 +1,26 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + +A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + +Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + +```ts +const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, +}); +``` diff --git a/.changeset/metal-planes-nail.md b/.changeset/metal-planes-nail.md new file mode 100644 index 0000000000..2cfa12a435 --- /dev/null +++ b/.changeset/metal-planes-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. diff --git a/.changeset/metal-rice-call.md b/.changeset/metal-rice-call.md new file mode 100644 index 0000000000..46e30dd26f --- /dev/null +++ b/.changeset/metal-rice-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Introduces the HasSubdomainsCard component that displays the subdomains of a given domain diff --git a/.changeset/mighty-apricots-taste.md b/.changeset/mighty-apricots-taste.md new file mode 100644 index 0000000000..80b70020cb --- /dev/null +++ b/.changeset/mighty-apricots-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. diff --git a/.changeset/modern-poems-mate.md b/.changeset/modern-poems-mate.md new file mode 100644 index 0000000000..06926daac9 --- /dev/null +++ b/.changeset/modern-poems-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Allow using notifications without users in the catalog diff --git a/.changeset/neat-socks-cheer.md b/.changeset/neat-socks-cheer.md new file mode 100644 index 0000000000..ffc76bec3f --- /dev/null +++ b/.changeset/neat-socks-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-node': minor +--- + +**BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. diff --git a/.changeset/old-tools-smell.md b/.changeset/old-tools-smell.md new file mode 100644 index 0000000000..4dbd65e779 --- /dev/null +++ b/.changeset/old-tools-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Add support for accessing extensions definitions provided by a plugin via `plugin.getExtension(...)`. For this to work the extensions must be defined using the v2 format, typically using an extension blueprint. diff --git a/.changeset/popular-panthers-hear.md b/.changeset/popular-panthers-hear.md new file mode 100644 index 0000000000..c31586ea82 --- /dev/null +++ b/.changeset/popular-panthers-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs redirect feature now includes a notification to the user before they are redirected. diff --git a/.changeset/pre.json b/.changeset/pre.json index 8b33f2b80b..4699170212 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -190,41 +190,62 @@ "@backstage/plugin-techdocs-common": "0.0.0" }, "changesets": [ + "angry-dolphins-camp", "big-eagles-grab", "blue-pumas-cheer", "breezy-jeans-tie", + "breezy-rings-fly", + "bright-donkeys-buy", "bright-trainers-brake", + "calm-crabs-drop", + "chilly-days-peel", "chilly-trains-sleep", + "clever-pans-brake", "cool-insects-remember", "cool-schools-vanish", "create-app-1722413762", "cuddly-zebras-crash", + "curvy-pillows-joke", + "cyan-shrimps-push", "dry-squids-tap", "dull-ghosts-double", "early-trees-dance", "eighty-emus-leave", + "eighty-jokes-deny", "eighty-mirrors-flow", + "fair-hairs-mix", + "fair-pumas-hang", + "fast-bulldogs-relax", "few-wasps-hug", "flat-plums-grow", "forty-ties-agree", "forty-ties-disagree", + "friendly-cherries-applaud", + "friendly-chicken-cry", "friendly-feet-refuse", "gentle-dryers-smile", "good-steaks-report", "green-planets-reflect", "grumpy-owls-suffer", + "healthy-timers-divide", "heavy-numbers-love", + "hip-fishes-guess", "hip-hairs-exist", + "khaki-lamps-peel", "late-games-protect", "light-pianos-exercise", "little-bulldogs-guess", "little-suns-fly", "mean-apricots-perform", + "metal-planes-nail", + "metal-rice-call", + "mighty-apricots-taste", "mighty-dolls-retire", "mighty-geckos-kiss", "modern-parrots-protect", "neat-bears-divide", "neat-gifts-join", + "neat-socks-cheer", "new-scissors-try", "nice-peas-retire", "nine-cherries-decide", @@ -236,16 +257,26 @@ "orange-gifts-protect", "pink-gorillas-brake", "plenty-tools-exist", + "purple-carrots-crash", "quick-roses-juggle", "rare-foxes-compete", "red-radios-promise", + "renovate-147ac48", "renovate-f04beb1", "rich-mugs-dress", "selfish-bees-think", "seven-eggs-admire", "shaggy-dodos-applaud", + "shy-games-poke", + "shy-waves-share", + "silly-candles-sin", + "silly-cycles-tan", "six-rats-kick", + "slow-ducks-rush", + "slow-ligers-drum", "slow-toes-jog", + "small-bottles-cough", + "small-ears-poke", "small-spoons-shout", "smooth-countries-relate", "soft-gorillas-refuse", @@ -258,15 +289,23 @@ "sweet-oranges-buy", "swift-kings-sparkle", "tall-snakes-fix", + "tasty-ads-rescue", "thick-hotels-know", + "thirty-adults-grab", "thirty-paws-hope", "tiny-oranges-pretend", + "tough-goats-hang", "tough-lies-repair", + "tricky-ducks-juggle", "two-emus-work", "violet-jokes-wave", + "warm-monkeys-marry", "wicked-bobcats-teach", "wild-eggs-exist", + "wise-spiders-walk", "witty-bears-behave", + "witty-geese-battle", + "witty-timers-marry", "young-games-visit", "young-peaches-shake" ] diff --git a/.changeset/purple-carrots-crash.md b/.changeset/purple-carrots-crash.md new file mode 100644 index 0000000000..62f90677b8 --- /dev/null +++ b/.changeset/purple-carrots-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. diff --git a/.changeset/real-lizards-sit.md b/.changeset/real-lizards-sit.md new file mode 100644 index 0000000000..747f041508 --- /dev/null +++ b/.changeset/real-lizards-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fix undefined in the title of Scaffolder Runs on the page load diff --git a/.changeset/renovate-147ac48.md b/.changeset/renovate-147ac48.md new file mode 100644 index 0000000000..257255dff1 --- /dev/null +++ b/.changeset/renovate-147ac48.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated dependency `@graphiql/react` to `^0.23.0`. diff --git a/.changeset/seven-days-film.md b/.changeset/seven-days-film.md new file mode 100644 index 0000000000..9d757b0611 --- /dev/null +++ b/.changeset/seven-days-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Fixed a type issue where plugin and modules depending on multiton services would not receive the correct type. diff --git a/.changeset/shaggy-mugs-return.md b/.changeset/shaggy-mugs-return.md new file mode 100644 index 0000000000..6af5d1d18f --- /dev/null +++ b/.changeset/shaggy-mugs-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Update configuration schema to match actual behavior diff --git a/.changeset/shy-games-poke.md b/.changeset/shy-games-poke.md new file mode 100644 index 0000000000..1d2abff39c --- /dev/null +++ b/.changeset/shy-games-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Add support for stream transport for debugging purposes diff --git a/.changeset/shy-waves-share.md b/.changeset/shy-waves-share.md new file mode 100644 index 0000000000..861590fd01 --- /dev/null +++ b/.changeset/shy-waves-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. diff --git a/.changeset/silly-candles-sin.md b/.changeset/silly-candles-sin.md new file mode 100644 index 0000000000..b1d55113fd --- /dev/null +++ b/.changeset/silly-candles-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. diff --git a/.changeset/silly-cycles-tan.md b/.changeset/silly-cycles-tan.md new file mode 100644 index 0000000000..517ce526ea --- /dev/null +++ b/.changeset/silly-cycles-tan.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. +This was already documented and supported by the plugin. diff --git a/.changeset/silly-scissors-turn.md b/.changeset/silly-scissors-turn.md new file mode 100644 index 0000000000..985a6fb4fd --- /dev/null +++ b/.changeset/silly-scissors-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Included permission config and enabled it out of the box diff --git a/.changeset/silver-pillows-begin.md b/.changeset/silver-pillows-begin.md new file mode 100644 index 0000000000..8fe1ccc230 --- /dev/null +++ b/.changeset/silver-pillows-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Internal type refactor. diff --git a/.changeset/six-mails-smell.md b/.changeset/six-mails-smell.md new file mode 100644 index 0000000000..3845c72626 --- /dev/null +++ b/.changeset/six-mails-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Support merging of `inputs` in extension blueprints, but stop merging `output`. In addition, the original factory in extension blueprints now returns a data container that both provides access to the returned data, but can also be forwarded as output. diff --git a/.changeset/slow-ducks-rush.md b/.changeset/slow-ducks-rush.md new file mode 100644 index 0000000000..c09f883e9c --- /dev/null +++ b/.changeset/slow-ducks-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. diff --git a/.changeset/slow-ligers-drum.md b/.changeset/slow-ligers-drum.md new file mode 100644 index 0000000000..867cc07cee --- /dev/null +++ b/.changeset/slow-ligers-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix null check in `isJsonObject` utility function for scaffolder review state component diff --git a/.changeset/small-bottles-cough.md b/.changeset/small-bottles-cough.md new file mode 100644 index 0000000000..40098a84c7 --- /dev/null +++ b/.changeset/small-bottles-cough.md @@ -0,0 +1,58 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + +We're looking for ways to make it possible to augment services without the need to replace the entire service. + +Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + +```diff +// @backstage/backend-defaults + ++ export const urlReaderFactoriesServiceRef = createServiceRef({ ++ id: 'core.urlReader.factories', ++ scope: 'plugin', ++ multiton: true, ++ }); + +... + +export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, ++ factories: urlReaderFactoriesServiceRef, + }, +- async factory({ config, logger }) { ++ async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, ++ factories, + }); + }, +}); +``` + +With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + +```ts +// packages/backend/index.ts +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; +... + +backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, +})); + +... + +``` diff --git a/.changeset/small-ears-poke.md b/.changeset/small-ears-poke.md new file mode 100644 index 0000000000..de906a8643 --- /dev/null +++ b/.changeset/small-ears-poke.md @@ -0,0 +1,7 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +**BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + +This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. diff --git a/.changeset/tasty-ads-rescue.md b/.changeset/tasty-ads-rescue.md new file mode 100644 index 0000000000..d522856bdd --- /dev/null +++ b/.changeset/tasty-ads-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Avoid excessive numbers of error listeners on cache clients diff --git a/.changeset/ten-penguins-roll.md b/.changeset/ten-penguins-roll.md new file mode 100644 index 0000000000..152bb1524b --- /dev/null +++ b/.changeset/ten-penguins-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': minor +--- + +By default, set notification as read when opening snackbar or web notification link diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md new file mode 100644 index 0000000000..0e798063c0 --- /dev/null +++ b/.changeset/thick-squids-drive.md @@ -0,0 +1,34 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +--- + +Added support for being able to override extension definitions. + +```tsx +const TestCard = EntityCardBlueprint.make({ + ... +}); + +TestCard.override({ + // override attachment points + attachTo: { id: 'something-else', input: 'overridden' }, + // extend the config schema + config: { + schema: { + newConfig: z => z.string().optional(), + } + }, + // override factory + *factory(originalFactory, { inputs, config }){ + const originalOutput = originalFactory(); + + yield coreExentsionData.reactElement( + + {originalOutput.get(coreExentsionData.reactElement)} + + ); + } +}); + +``` diff --git a/.changeset/thin-carrots-eat.md b/.changeset/thin-carrots-eat.md new file mode 100644 index 0000000000..570f21f83d --- /dev/null +++ b/.changeset/thin-carrots-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for action github:pages and improved its test cases diff --git a/.changeset/thirty-adults-grab.md b/.changeset/thirty-adults-grab.md new file mode 100644 index 0000000000..ed986a8289 --- /dev/null +++ b/.changeset/thirty-adults-grab.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-catalog-node': patch +--- + +Explicit declare if the service ref accepts `single` or `multiple` implementations. diff --git a/.changeset/tiny-dodos-prove-2.md b/.changeset/tiny-dodos-prove-2.md new file mode 100644 index 0000000000..91ac021c67 --- /dev/null +++ b/.changeset/tiny-dodos-prove-2.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Deprecate existing `ExtensionCreators` in favour of their new Blueprint counterparts. diff --git a/.changeset/tiny-dodos-prove.md b/.changeset/tiny-dodos-prove.md new file mode 100644 index 0000000000..6392e5050e --- /dev/null +++ b/.changeset/tiny-dodos-prove.md @@ -0,0 +1,9 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Refactor `.make` method on Blueprints into two different methods, `.make` and `.makeWithOverrides`. + +When using `createExtensionBlueprint` you can define parameters for the factory function, if you wish to take advantage of these parameters you should use `.make` when creating an extension instance of a Blueprint. If you wish to override more things other than the standard `attachTo`, `name`, `namespace` then you should use `.makeWithOverrides` instead. + +`.make` is reserved for simple creation of extension instances from Blueprints using higher level parameters, whereas `.makeWithOverrides` is lower level and you have more control over the final extension. diff --git a/.changeset/tough-goats-hang.md b/.changeset/tough-goats-hang.md new file mode 100644 index 0000000000..2608c03b2e --- /dev/null +++ b/.changeset/tough-goats-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket diff --git a/.changeset/tricky-ducks-juggle.md b/.changeset/tricky-ducks-juggle.md new file mode 100644 index 0000000000..48c0d215bd --- /dev/null +++ b/.changeset/tricky-ducks-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added configuration for the `packages` options to config schema diff --git a/.changeset/tricky-rules-destroy.md b/.changeset/tricky-rules-destroy.md new file mode 100644 index 0000000000..196702f8c4 --- /dev/null +++ b/.changeset/tricky-rules-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Fix TechDocs Edit URL for nested docs diff --git a/.changeset/warm-monkeys-marry.md b/.changeset/warm-monkeys-marry.md new file mode 100644 index 0000000000..5ea8deae01 --- /dev/null +++ b/.changeset/warm-monkeys-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-sentry': patch +--- + +Added test cases for sentry:project:create examples diff --git a/.changeset/wise-spiders-walk.md b/.changeset/wise-spiders-walk.md new file mode 100644 index 0000000000..65793041c5 --- /dev/null +++ b/.changeset/wise-spiders-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. diff --git a/.changeset/witty-geese-battle.md b/.changeset/witty-geese-battle.md new file mode 100644 index 0000000000..082784cc8b --- /dev/null +++ b/.changeset/witty-geese-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. diff --git a/.changeset/witty-queens-run.md b/.changeset/witty-queens-run.md new file mode 100644 index 0000000000..2d869caa91 --- /dev/null +++ b/.changeset/witty-queens-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Notification email processor supports allowing or denying specific email addresses from receiving notifications diff --git a/.changeset/witty-timers-marry.md b/.changeset/witty-timers-marry.md new file mode 100644 index 0000000000..4193acb6fc --- /dev/null +++ b/.changeset/witty-timers-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bitbucket-cloud-common': patch +--- + +Added method `listBranchesByRepository` to `BitbucketCloudClient` diff --git a/.changeset/young-birds-push.md b/.changeset/young-birds-push.md new file mode 100644 index 0000000000..54afa48aaf --- /dev/null +++ b/.changeset/young-birds-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Introduce an optional spec.type attribute on the Domain and System entity kinds diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 42b0e4895c..bad61de7ff 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -245,6 +245,7 @@ Monorepo monorepos msgraph msw +multiton mutex mutexes mysql diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 2f93b67616..e9da9f0398 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -51,7 +51,7 @@ jobs: - run: yarn build-storybook - - uses: chromaui/action@5991575112b472a4fa85429f29cc17e2fb83a2a2 # v11 + - uses: chromaui/action@fdbe7756d4dbf493e2fbb822df73be7accd07e1c # v11 with: token: ${{ secrets.GITHUB_TOKEN }} # projectToken intentionally shared to allow collaborators to run Chromatic on forks diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 6eb52e5708..dbebf3056c 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -193,7 +193,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); backend.add(customAuth); ``` -Check out [the naming patterns article](../backend-system/architecture/07-naming-patterns.md) for what rules +Check out [the naming patterns article](../backend-system/architecture/08-naming-patterns.md) for what rules apply regarding how to form valid IDs. In this example we also put the module declaration directly in `packages/backend/src/index.ts` but that's just for simplicity. You can place it anywhere you like, including in other packages, and diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 95e3d5305a..57f6b20b73 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -38,7 +38,7 @@ export const fooServiceRef = createServiceRef({ The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime. -When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'.'`. For more naming patterns surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page. +When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'.'`. For more naming patterns surrounding services, see the [naming patterns](./08-naming-patterns.md#services) page. A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably. diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index af59c2a48a..5aef71e24f 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -10,7 +10,7 @@ Plugins provide the actual base features of a Backstage backend. Each plugin ope ## Defining a Plugin -Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./07-naming-patterns.md) about proper naming patterns. +Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./08-naming-patterns.md) about proper naming patterns. ```ts // plugins/example-backend/src/plugin.ts diff --git a/docs/backend-system/architecture/06-modules.md b/docs/backend-system/architecture/06-modules.md index eb00e737e3..010f36109c 100644 --- a/docs/backend-system/architecture/06-modules.md +++ b/docs/backend-system/architecture/06-modules.md @@ -52,7 +52,7 @@ This allows you to install the module in your backend instance by just referenci ```ts backend.add( - import('backstage-plugin-catalog-backend-module-example-processor'), + import('@internal/backstage-plugin-catalog-backend-module-example-processor'), ); ``` diff --git a/docs/backend-system/architecture/07-feature-loaders.md b/docs/backend-system/architecture/07-feature-loaders.md new file mode 100644 index 0000000000..cf52a1feb3 --- /dev/null +++ b/docs/backend-system/architecture/07-feature-loaders.md @@ -0,0 +1,101 @@ +--- +id: feature-loaders +title: Backend Feature Loaders +sidebar_label: Feature Loaders +# prettier-ignore +description: Backend feature loaders +--- + +Backend feature loaders are used to programmatically select and install features in a Backstage backend. They can service a wide range of use cases, such as enabling or disabling features based on static configuration, dynamically load features at runtime, or conditionally load features based on the state of a system. + +Feature loaders are defined using the `createBackendFeatureLoader` function, exported by `@backstage/backend-plugin-api`. It accepts a `loader` function, as well as an optional `deps` object for declaring service dependencies. Unlike plugins and modules, feature loaders are limited to only depending on root-scoped services, but that still allows access to for example the [root config](../core-services/root-config.md) and [root logger](../core-services/root-logger.md) services. + +The `loader` function can be defined in many different ways, with the main requirement being that it returns a list of `BackendFeature`s in some form. A backend feature is the kind of object that you can pass to `backend.add(...)`, for example services factories, plugins, modules, or even other feature loaders. The `loader` function can be synchronous or asynchronous, and can be defined as a generator function to allow for more complex logic. + +## Examples + +The following are a few example of how feature loaders can be used: + +### Simple list of features + +A feature loader can simply return a list of features to be installed: + +```ts +export default createBackendFeatureLoader({ + loader() { + return [ + import('@backstage/plugin-search-backend/alpha'), + import('@backstage/plugin-search-backend-module-catalog/alpha'), + import('@backstage/plugin-search-backend-module-explore/alpha'), + import('@backstage/plugin-search-backend-module-techdocs/alpha'), + ]; + }, +}); +``` + +It can also encapsulate a collection of custom features: + +```ts +export default createBackendFeatureLoader({ + // Async loader is fine too + async loader() { + return [ + createBackendPlugin({ + ... + }), + createBackendModule({ + ... + }), + ] + }, +}); +``` + +### Conditional loading + +A feature loader can access root-scoped services, such as the config service. This allows for conditional loading of features based on configuration. It is often convenient to use a generator function for this purpose: + +```ts +export default createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + // The `*` in front of the function name makes it a generator function + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, +}); +``` + +### Dynamic logic + +A feature loader can also be asynchronous, and for example fetch data from an external source to determine which features to load: + +```ts +export default createBackendFeatureLoader({ + // The `async *` in front of the function name makes it an async generator function. + async *loader() { + const localMetadata = await readMetadataFromDisk(); + + if (localMetadata.enableSearch) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + + const remoteMetadata = await fetchMetadata(); + + if (remoteMetadata.enableExplore) { + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + } + if (remoteMetadata.enableTechDocs) { + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + } + }, +}); +``` diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/08-naming-patterns.md similarity index 100% rename from docs/backend-system/architecture/07-naming-patterns.md rename to docs/backend-system/architecture/08-naming-patterns.md diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 379ce19587..4dfca00d4e 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -68,7 +68,7 @@ that's specific to your plugin. In the example above, the logger might tag messages with your plugin ID, and the HTTP router might prefix API routes with your plugin ID, depending on the implementation used. -See [the article on naming patterns](../architecture/07-naming-patterns.md) for +See [the article on naming patterns](../architecture/08-naming-patterns.md) for details on how to best choose names/IDs for plugins and related backend system items. @@ -124,7 +124,7 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({ export { catalogModuleExampleCustomProcessor as default } from './module'; ``` -See [the article on naming patterns](../architecture/07-naming-patterns.md) for +See [the article on naming patterns](../architecture/08-naming-patterns.md) for details on how to best choose names/IDs for modules and related backend system items. diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index 733b7b8277..36bb276920 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -17,7 +17,8 @@ The following example is how you can override the root logger service to add add ```ts import { coreServices } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; +import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; +import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootConfig'; const backend = createBackend(); @@ -43,6 +44,12 @@ backend.add( transports: [new transports.Console()], }); + const secretEnumerator = await createConfigSecretEnumerator({ + logger, + }); + logger.addRedactions(secretEnumerator(config)); + config.subscribe?.(() => logger.addRedactions(secretEnumerator(config))); + return logger; }, }), diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 416eb27fbd..57c2283586 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1243,6 +1243,18 @@ system belongs to, e.g. `artists`. This field is optional. | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | | [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | +### `spec.type` [optional] + +The type of system. There is currently no enforced set of values for this field, +so it is left up to the adopting organization to choose a nomenclature that +matches their catalog hierarchy. This field is optional. + +Some common values for this field could be: + +- `product` +- `service` +- `feature-set` + ## Kind: Domain Describes the following entity kind: @@ -1303,6 +1315,18 @@ which the domain is a part, e.g. `audio`. This field is optional. | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | | [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | +### `spec.type` [optional] + +The type of domain. There is currently no enforced set of values for this field, +so it is left up to the adopting organization to choose a nomenclature that +matches their catalog hierarchy. This field is optional. + +Some common values for this field could be: + +- `product-area` +- `product-group` +- `bundle` + ## Kind: Location Describes the following entity kind: diff --git a/docs/features/software-templates/dry-run-testing.md b/docs/features/software-templates/dry-run-testing.md new file mode 100644 index 0000000000..dfabad265e --- /dev/null +++ b/docs/features/software-templates/dry-run-testing.md @@ -0,0 +1,79 @@ +--- +id: dry-run-testing +title: Dry Run Testing +description: How to enable and implement dry run testing in actions +--- + +Scaffolder templates can be tested using the dry run feature of scaffolder actions. This allows you to simulate the effects of running a scaffolder action without making any actual changes to your environment, for example creating a webhook in Github. Once dry run is enabled in the scaffolder action, you can add handling to actions you use in your scaffolder templates to define how an action should operate in a dry run scenario. + +## Enabling dry run testing + +To enable dry run for your scaffolder action you need to add `supportsDryRun: true` to the configuration object of `createTemplateAction` in the function where the behavior of your action is defined: + +```typescript +export function exampleAction() { + return createTemplateAction<{ + example: string; + }>({ + id: 'action:example', + description: 'Example action', + schema: { + input: { + type: 'object', + properties: { + example: { + title: 'example', + type: 'string', + }, + }, + }, + }, + supportsDryRun: true, + async handler(ctx) { + ... + }, + }); +} +``` + +## Adding handling for dry run + +To add handling for dry run functionality you need to add a check for `ctx.isDryRun` inside the handler of the configuration object which is being passed into `createTemplateAction` in the function where the behavior of your action is defined. Once the check is successful, you can perform the desired actions expected in a dry run, e.g. outputting non-sensitive inputs. + +```typescript +async handler(ctx) { + ... + + // If this is a dry run, log and return + if (ctx.isDryRun) { + ctx.logger.info(`Dry run complete`); + return; + } + + ... + }, +``` + +## Testing dry run handling + +You will also need to add tests for the dry run handling, for example: + +```typescript + it('should not perform action during dry run', async () => { + ... + + // Create the context object with the necessary properties for a dry run + const ctx = { + ...mockContext, + isDryRun: true, + input: { + ... + }, + }; + + // Call the handler with the context + await action.handler(ctx); + + expect(...); + }); +``` diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 7f0e5b225f..1a024569a6 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -441,3 +441,30 @@ repoUrl: `secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +## RepoBranchPicker + +The input props that can be specified under `ui:options` for the `RepoBranchPicker` field extension. + +### `requestUserCredentials` + +If defined will request user credentials to auth against the given SCM platform. + +```yaml +repoUrl: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN + additionalScopes: + github: + - workflow:write +``` + +`secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. + +The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +If you're also using the `RepoUrlPicker` field extension, you should simply duplicate this part from there. diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 075fe0bb08..13dbe6da4b 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -477,6 +477,34 @@ template can be published to multiple providers. Note, that you will need to configure an [authentication provider](../../auth/index.md#configuring-authentication-providers), alongside the [`ScmAuthApi`](../../auth/index.md#scaffolder-configuration-software-templates) for your source code management (SCM) service to make this feature work. +### The Repository Branch Picker + +Similar to the repository picker, there is a picker for branches to support autocompletion. A full example could look like this: + +```yaml +- title: Choose a branch + required: + - repoBranch + properties: + repoBranch: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN +``` + +Passing the `requestUserCredentials` object is required for autocompletion to work. +If you're also using the repository picker, you should simply duplicate this part from there. +For more information regarding the `requestUserCredentials` object, please refer to the [Using the Users `oauth` token](#using-the-users-oauth-token) section under [The Repository Picker](#the-repository-picker). + +For a list of all possible `ui:options` input props for `RepoBranchPicker`, please visit [here](./ui-options-examples.md#repobranchpicker). + +The `RepoBranchPicker` is a custom field that we provide part of the +`plugin-scaffolder`. You can provide your own custom fields by +[writing your own Custom Field Extensions](./writing-custom-field-extensions.md) + ### Accessing the signed-in users details Sometimes when authoring templates, you'll want to access the user that is running the template, and get details from the profile or the users `Entity` in the Catalog. diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index a861a50c87..8c2348972c 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -20,7 +20,7 @@ techdocs: # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running # into Docker in Docker situation. Read more here - # https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional + # https://backstage.io/docs/features/techdocs/getting-started/#disabling-docker-in-docker-situation-optional runIn: 'docker' diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 9c6617a06f..9db4d007b6 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -101,23 +101,26 @@ export const IndexPage = () => { return (

Index Page

- - See "Foo" details - + {/* highlight-next-line */} + {getDetailsPath && ( + + See "Foo" details + + )}
); }; ``` -We use the `useRouteRef` hook to create a link generator function that returns the details page path. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page. +We use the `useRouteRef` hook to create a link generator function that returns the details page path. First we need to check whether the route is available, the link generator function will be `undefined` if it isn't. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page. Let's see how the details page can get the parameters from the URL: @@ -176,8 +179,11 @@ export const IndexPage = () => { return (

Index Page

- {/* highlight-next-line */} - Create Component + {/* highlight-start */} + {getCreateComponentPath && ( + Create Component + )} + {/* highlight-end */}
); }; @@ -289,42 +295,6 @@ export const createComponentExternalRouteRef = createExternalRouteRef({ }); ``` -### Optional External Route References - -It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. - -```tsx title="plugins/catalog/src/routes.ts" -import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; - -export const createComponentExternalRouteRef = createExternalRouteRef({ - // highlight-next-line - optional: true, -}); -``` - -When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: - -```tsx title="plugins/catalog/src/components/IndexPage.tsx" -import React from 'react'; -import { useRouteRef } from '@backstage/frontend-plugin-api'; -import { createComponentExternalRouteRef } from '../routes'; - -export const IndexPage = () => { - const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef); - return ( -
-

Index Page

- {/* Rendering the link only if the getCreateComponentPath is defined */} - {/* highlight-start */} - {getCreateComponentPath && ( - Create Component - )} - {/* highlight-end */} -
- ); -}; -``` - ## Sub Route References The last kind of route ref that can be created is a `SubRouteRef`, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you have a page that internally is mounted at a sub route of a page extension component, and you want other plugins to be able to route to that page. And they can be a useful utility to handle routing within a plugin itself as well. @@ -368,17 +338,21 @@ import { DetailsPage } from './DetailsPage'; export const IndexPage = () => { const { pathname } = useLocation(); + + // highlight-start const getIndexPath = useRouteRef(indexRouteRef); const getDetailsPath = useRouteRef(detailsSubRouteRef); + // highlight-end + return (

Index Page

{/* Linking to the details sub route */} - {pathname === getIndexPath() ? ( - // highlight-start + {/* highlight-start */} + {pathname === getIndexPath?.() ? ( { > Show details - // highlight-end + {/* highlight-end */} ) : ( - // highlight-next-line - Hide details + {/* highlight-next-line */} + Hide details )} {/* Registering the details sub route */} + {/* highlight-next-line */} } />
diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 4dc6c24134..6659bec182 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -28,6 +28,51 @@ app: 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. +Please note that the env value MUST be specified at build time + +:::note +In case after a proper configuration, the events still are not being captured: Copy and paste this section in to your `packages/app/public/index.html` under the `` tag. + +```html +<% if (config.has('app.datadogRum')) { %> + +<% } %> +``` + The `clientToken` and `applicationId` are generated from the Datadog RUM page following [these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/). diff --git a/docs/notifications/index.md b/docs/notifications/index.md new file mode 100644 index 0000000000..95d77a9b7f --- /dev/null +++ b/docs/notifications/index.md @@ -0,0 +1,320 @@ +--- +id: index +title: Getting Started +description: How to get started with the notifications and signals +--- + +The Backstage Notifications System provides a way for plugins and external services to send notifications to Backstage users. +These notifications are displayed in the dedicated page of the Backstage frontend UI or by frontend plugins per specific scenarios. +Additionally, notifications can be sent to external channels (like email) via "processors" implemented within plugins. + +Notifications can be optionally integrated with the signals (a push mechanism) to ensure users receive them immediately. + +### Upgrade to the latest version of Backstage + +To ensure your version of Backstage has all the latest notifications and signals related functionality, it’s important to upgrade to the latest version. The [Backstage upgrade helper](https://backstage.github.io/upgrade-helper/) is a great tool to help ensure that you’ve made all the necessary changes during the upgrade! + +## About notifications + +Notifications are messages sent to either individual users or groups. +They are not intended for inter-process communication of any kind. + +There are two basic types of notifications: + +- **Broadcast**: Messages sent to all users of Backstage. +- **Entity**: Messages delivered to specific listed entities, such as Users or Groups. + +Example of use-cases: + +- System-wide announcements or alerts +- Notifications for component owners: e.g., build failures, successful deployments, new vulnerabilities +- Notifications for individuals: e.g., updates you have subscribed to, new required training courses +- Notifications pertaining to a particular entity in the catalog: A notification might apply to an entity and the owning team. + +## Installation in Older Environments + +Newer versions of instances created by the create-app have both the notifications and signals plugins included by default, this section can be skipped right to the Configuration. + +Following installation instructions are valid for enabling the plugins in older environments. + +### Add Notifications Backend + +```bash +yarn workspace backend add @backstage/plugin-notifications-backend +``` + +Add the notifications to your `backend/src/index.ts`: + +```ts +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-notifications-backend')); +``` + +### Add Notifications Frontend + +```bash +yarn workspace app add @backstage/notifications +``` + +To add the notifications main menu, add following to your `packages/app/src/components/Root/Root.tsx`: + +```tsx +import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; + + + + + // ... + + + +; +``` + +Also add the route to notifications to `packages/app/src/App.tsx`: + +```tsx +import { NotificationsPage } from '@backstage/plugin-notifications'; + + + // ... + } /> +; +``` + +### Optional: Add Signals Backend + +Optionally add Signals to your backend by + +```bash +yarn workspace backend add @backstage/plugin-signals-backend +``` + +Add the signals to your `backend/src/index.ts`: + +```ts +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-signals-backend')); +``` + +### Optional: Signals Frontend + +The use of signals is optional but improves user experience. + +Start with: + +```bash +yarn workspace app add @backstage/plugin-signals +``` + +To install the plugin, you have to add the following to your `packages/app/src/plugins.ts`: + +```ts +export { signalsPlugin } from '@backstage/plugin-signals'; +``` + +And make sure that your `packages/app/src/App.tsx` contains: + +```ts +import * as plugins from './plugins'; + +const app = createApp({ + // ... + plugins: Object.values(plugins), + // ... +}); +``` + +If the signals plugin is properly configured, it will be automatically discovered by the notifications plugin and used. + +## Configuration + +### Notifications Backend + +The Notifications backend plugin provides an API to create notifications, list notifications per logged-in user, and search based on parameters. + +The plugin uses a relational [database](https://backstage.io/docs/getting-started/config/database) for persistence, no specifics are introduced in this context. + +No additional configuration in the app-config is needed, except for optional additional modules for `processors`. + +### Notifications Frontend + +The recipients of notifications have to be entities in the catalog, e.g. of the User or Group kind. + +Otherwise no specific configuration is needed for the front-end notifications plugin. + +All parametrization is done through component properties, such as the `NotificationsSidebarItem`, which can be used as an active left-side menu item in the front-end. + +![Notifications Page](notificationsPage.png) + +In the `packages/app/src/components/Root/Root.tsx`, tweak the [properties](https://backstage.io/docs/reference/plugin-notifications.notificationssidebaritem) of the `` per specific needs. + +## Use + +New notifications can be sent either by a backend plugin or an external service through the REST API. + +### Backend + +Regardless of technical feasibility, a backend plugin should avoid directly accessing the notifications REST API. +Instead, it should integrate with the `@backstage/plugin-notifications-node` to `send` (create) a new notification. + +The reasons for this approach include the propagation of authorization in the API request and improved maintenance and backward compatibility in the future. + +```ts +import { notificationService } from '@backstage/plugin-notifications-node'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'myPlugin', + register(env) { + env.registerInit({ + deps: { + // ... + notificationService: notificationService, + }, + async init({ config, logger, httpRouter, notificationService }) { + httpRouter.use( + await createRouter({ + // ... + notificationService, + }), + ); + }, + }); + }, +}); +``` + +To emit a new notification: + +```ts +notificationService.send({ + recipients /* of the broadcast or entity type */, + payload /* actual message */, +}); +``` + +Refer the [API documentation](https://github.com/backstage/backstage/blob/master/plugins/notifications-node/api-report.md) for further details. + +### Signals + +The use of signals with notifications is optional but generally enhances user experience and performance. + +When a notification is created, a new signal is emitted to a general-purpose message bus to announce it to subscribed listeners. + +The frontend maintains a persistent connection (WebSocket) to receive these announcements from the notifications channel. +The specific details of the updated or created notification should be retrieved via a request to the notifications API, except for new notifications, where the payload is included in the signal for performance reasons. + +In a frontend plugin, to subscribe for notifications' signals: + +```ts +import { useSignal } from '@backstage/plugin-signals-react'; + +const { lastSignal } = useSignal('notifications'); + +React.useEffect(() => { + /* ... */ +}, [lastSignal, notificationsApi]); +``` + +### Consuming Notifications + +In a front-end plugin, the simplest way to query a notification is by its ID: + +```ts +import { useApi } from '@backstage/core-plugin-api'; +import { notificationsApiRef } from '@backstage/plugin-notifications'; + +const notificationsApi = useApi(notificationsApiRef); + +notificationsApi.getNotification(yourId); + +// or with connection to signals: +notificationsApi.getNotification(lastSignal.notification_id); +``` + +### Extending Notifications via Processors + +The notifications can be extended with `NotificationProcessor`. These processors allow to decorate notifications before they are sent or/and send the notifications to external services. + +Depending on the needs, a processor can modify the content of a notification or route it to different systems like email, Slack, or other services. + +A good example of how to write a processor is the [Email Processor](https://github.com/backstage/backstage/tree/master/plugins/notifications-backend-module-email). + +Start off by creating a notification processor: + +```ts +import { Notification } from '@backstage/plugin-notifications-common'; +import { NotificationProcessor } from '@backstage/plugin-notifications-node'; + +class MyNotificationProcessor implements NotificationProcessor { + async decorate(notification: Notification): Promise { + if (notification.origin === 'plugin-my-plugin') { + notification.payload.icon = 'my-icon'; + } + return notification; + } + + async send(notification: Notification): Promise { + nodemailer.sendEmail({ + from: 'backstage', + to: 'user', + subject: notification.payload.title, + text: notification.payload.description, + }); + } +} +``` + +Both of the processing functions are optional, and you can implement only one of them. + +Add the notification processor to the notification system by: + +```ts +import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; +import { Notification } from '@backstage/plugin-notifications-common'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'myPlugin', + register(env) { + env.registerInit({ + deps: { + notifications: notificationsProcessingExtensionPoint, + // ... + }, + async init({ notifications }) { + // ... + notifications.addProcessor(new MyNotificationProcessor()); + }, + }); + }, +}); +``` + +### External Services + +When the emitter of a notification is a Backstage backend plugin, it is mandatory to use the integration via `@backstage/plugin-notifications-node` as described above. + +If the emitter is a service external to Backstage, an HTTP POST request can be issued directly to the API, assuming that authentication is properly configured. +Refer to the [service-to-service auth documentation](https://backstage.io/docs/auth/service-to-service-auth) for more details, focusing on the Static Tokens section for the simplest setup option. + +An example request for creating a broadcast notification might look like: + +```bash +curl -X POST https://[BACKSTAGE_BACKEND]/api/notifications -H "Content-Type: application/json" -H "Authorization: Bearer YOUR_BASE64_SHARED_KEY_TOKEN" -d '{"recipients":{"type":"broadcast"},"payload": {"title": "Title of broadcast message","link": "http://foo.com/bar","severity": "high","topic": "The topic"}}' +``` + +## Additional info + +An example of a backend plugin sending notifications can be found in https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-notifications. + +Sources of the notifications and signal plugins: + +- https://github.com/backstage/backstage/blob/master/plugins/notifications + +- https://github.com/backstage/backstage/blob/master/plugins/notifications-backend + +- https://github.com/backstage/backstage/blob/master/plugins/notifications-node + +- https://github.com/backstage/backstage/blob/master/plugins/signals-react diff --git a/docs/notifications/notificationsPage.png b/docs/notifications/notificationsPage.png new file mode 100644 index 0000000000..7c96ab77af Binary files /dev/null and b/docs/notifications/notificationsPage.png differ diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index 60458aaf4d..4d673e3b4f 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -199,7 +199,8 @@ const routes = ( - {/* highlight-add-end */}} + {/* highlight-add-end */} + }> {/* ... */} diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index e13653706f..324e6751d4 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -17,7 +17,12 @@ See how to install Datadog Events in your app ### New Backend -The backend supplies a central logging service, [`rootLogger`](../backend-system/core-services/root-logger.md), as well as a plugin based logger, [`logger`](../backend-system/core-services/logger.md) from `coreServices`. To add additional granularity to your logs, you can create children from the plugin based logger, using the `.child()` method and provide is with JSON data. For example, if you wanted to log items for a specific span in your plugin, you could do +The backend supplies a central logging service, +[`rootLogger`](../backend-system/core-services/root-logger.md), as well as a plugin +based logger, [`logger`](../backend-system/core-services/logger.md) from `coreServices`. +To add additional granularity to your logs, you can create children from the plugin +based logger, using the `.child()` method and provide is with JSON data. For example, +if you wanted to log items for a specific span in your plugin, you could do ```ts export function createRouter({ logger }) { @@ -37,7 +42,9 @@ export function createRouter({ logger }) { } ``` -You can also add additional metadata to all logs for your Backstage instance by overriding the `rootLogger` implementation, you can see an example in [the `logger` docs](../backend-system/core-services/logger.md#configuring-the-service). +You can also add additional metadata to all logs for your Backstage instance by +overriding the `rootLogger` implementation, you can see an example in +[the `rootLogger` docs](../backend-system/core-services/root-logger.md#configuring-the-service). ### Old Backend @@ -63,9 +70,19 @@ An example log line could look as follows: ## Health Checks -### New Backend +### New Backend (post 1.29.0) -The new backend is moving towards health checks being plugin-based, as such there is no current plugin for providing a health check route. You can add this yourself easily though, +The new backend provides a `RootHealthService` which implements +`/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints +to provide health checks for the entire backend instance. + +You can read more about this new service and how to customize it in the +[Root Health Service documentation](../backend-system/core-services/root-health.md). + +### New Backend (pre 1.29.0) + +The new backend is moving towards health checks being plugin-based, as such there is no +current plugin for providing a health check route. You can add this yourself easily though, ```ts import { diff --git a/docs/releases/v1.30.0-next.1-changelog.md b/docs/releases/v1.30.0-next.1-changelog.md index 2f7153fe5c..1d93235740 100644 --- a/docs/releases/v1.30.0-next.1-changelog.md +++ b/docs/releases/v1.30.0-next.1-changelog.md @@ -4,10 +4,6 @@ Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1](h ## @backstage/core-app-api@1.14.2-next.0 -# @backstage/core-app-api - -## 1.14.1-next.0 - ### Patch Changes - 9a46a81: The request to delete the session cookie when running the app in protected mode is now done with a plain `fetch` rather than `FetchApi`. This fixes a bug where the app would immediately try to sign-in again when removing the cookie during logout. @@ -16,1891 +12,7 @@ Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1](h - @backstage/core-plugin-api@1.9.3 - @backstage/types@1.1.1 - @backstage/version-bridge@1.0.8 - -## 1.14.0 - -### Minor Changes - -- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. - - ```yaml - app: - routes: - bindings: - # This has the effect of removing the button for registering new - # catalog entities in the scaffolder template list view - scaffolder.registerComponent: false - ``` - -### Patch Changes - -- db2e2d5: Updated config schema to support app.routes.bindings -- Updated dependencies - - @backstage/config@1.2.0 - - @backstage/core-plugin-api@1.9.3 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.13.1-next.1 - -### Patch Changes - -- db2e2d5: Updated config schema to support app.routes.bindings -- Updated dependencies - - @backstage/core-plugin-api@1.9.3 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.13.0-next.0 - -### Minor Changes - -- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. - - ```yaml - app: - routes: - bindings: - # This has the effect of removing the button for registering new - # catalog entities in the scaffolder template list view - scaffolder.registerComponent: false - ``` - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.9.3 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.12.6 - -### Patch Changes - -- 35fbe09: Added support for configuration of route bindings through static configuration, and default targets for external route refs. - - In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: - - ```yaml - app: - routes: - bindings: - catalog.createComponent: catalog-import.importPage - ``` - - Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. - - The equivalent of the above configuration in code is the following: - - ```ts - const app = createApp({ - // ... - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: catalogImportPlugin.routes.importPage, - }); - }, - }); - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.9.3 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.12.6-next.0 - -### Patch Changes - -- 35fbe09: Added support for configuration of route bindings through static configuration, and default targets for external route refs. - - In addition to configuring route bindings through code, it is now also possible to configure route bindings under the `app.routes.bindings` key, for example: - - ```yaml - app: - routes: - bindings: - catalog.createComponent: catalog-import.importPage - ``` - - Each key in the route binding object is of the form `.`, where the route name is key used in the `externalRoutes` object passed to `createPlugin`. The value is of the same form, but with the name taken from the plugin `routes` option instead. - - The equivalent of the above configuration in code is the following: - - ```ts - const app = createApp({ - // ... - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: catalogImportPlugin.routes.importPage, - }); - }, - }); - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.9.3-next.0 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.8 - -## 1.12.5 - -### Patch Changes - -- 1bed9a3: The Backstage identity session expiration check will no longer fall back to using the provider expiration. This was introduced to smooth out the rollout of Backstage release 1.18, and is no longer needed. - -## 1.12.4 - -### Patch Changes - -- c884b9a: The app is now aware of if it is being served from the `app-backend` with a separate public and protected bundles. When in protected mode the app will now continuously refresh the session cookie, as well as clear the cookie if the user signs out. -- abfbcfc: Updated dependency `@testing-library/react` to `^15.0.0`. -- cb1e3b0: Updated dependency `@testing-library/dom` to `^10.0.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.9.2 - - @backstage/version-bridge@1.0.8 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - -## 1.12.4-next.0 - -### Patch Changes - -- c884b9a: The app is now aware of if it is being served from the `app-backend` with a separate public and protected bundles. When in protected mode the app will now continuously refresh the session cookie, as well as clear the cookie if the user signs out. -- Updated dependencies - - @backstage/config@1.2.0 - - @backstage/core-plugin-api@1.9.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.3 - -### Patch Changes - -- e8f026a: Use ESM exports of react-use library -- Updated dependencies - - @backstage/core-plugin-api@1.9.1 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.2 - -### Patch Changes - -- e8f026a: Use ESM exports of react-use library -- Updated dependencies - - @backstage/core-plugin-api@1.9.1 - - @backstage/config@1.2.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.2.0 - - @backstage/core-plugin-api@1.9.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.1-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.2.0-next.1 - - @backstage/core-plugin-api@1.9.1-next.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.1.2-next.0 - - @backstage/core-plugin-api@1.9.1-next.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.0 - -### Minor Changes - -- f919be9: Added a utility API for VMware Cloud auth; the API ref is available in the - `@backstage/core-plugin-api` and `@backstage/frontend-plugin-api` packages, the - implementation is in `@backstage/core-app-api` and a factory has been added to - `@backstage/app-defaults`. - -### Patch Changes - -- 9aac2b0: Use `--cwd` as the first `yarn` argument -- 8fe56a8: Widen `@types/react` dependency range to include version 18. -- 7da67ce: Change `defaultScopes` for Bitbucket auth from invalid `team` to `account`. -- Updated dependencies - - @backstage/core-plugin-api@1.9.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.12.0-next.1 - -### Minor Changes - -- f919be9: Added a utility API for VMware Cloud auth; the API ref is available in the - `@backstage/core-plugin-api` and `@backstage/frontend-plugin-api` packages, the - implementation is in `@backstage/core-app-api` and a factory has been added to - `@backstage/app-defaults`. - -### Patch Changes - -- 9aac2b0: Use `--cwd` as the first `yarn` argument -- 8fe56a8: Widen `@types/react` dependency range to include version 18. -- Updated dependencies - - @backstage/core-plugin-api@1.9.0-next.1 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.4-next.0 - -### Patch Changes - -- 7da67ce: Change `defaultScopes` for Bitbucket auth from invalid `team` to `account`. -- Updated dependencies - - @backstage/core-plugin-api@1.8.3-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.3 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.2 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.3-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.2-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.2 - -### Patch Changes - -- 3e358b0: Added deprecation warning for React Router v6 beta, please make sure you have migrated your apps to use React Router v6 stable as support for the beta version will be removed. See the [migration tutorial](https://backstage.io/docs/tutorials/react-router-stable-migration) for more information. -- 0cbb03b: Fixing regular expression ReDoS with zod packages. Upgrading to latest. ref: -- Updated dependencies - - @backstage/core-plugin-api@1.8.1 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.2-next.1 - -### Patch Changes - -- 3e358b0dff: Added deprecation warning for React Router v6 beta, please make sure you have migrated your apps to use React Router v6 stable as support for the beta version will be removed. See the [migration tutorial](https://backstage.io/docs/tutorials/react-router-stable-migration) for more information. -- Updated dependencies - - @backstage/core-plugin-api@1.8.1-next.1 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.8.1-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.7 - -## 1.11.1 - -### Patch Changes - -- 6c2b872153: Add official support for React 18. -- 89d13e5618: Add current and default scopes when refreshing session -- 9ab0572217: Add component data `core.type` marker for `AppRouter` and `FlatRoutes`. -- Updated dependencies - - @backstage/core-plugin-api@1.8.0 - - @backstage/version-bridge@1.0.7 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - -## 1.11.1-next.0 - -### Patch Changes - -- 6c2b872153: Add official support for React 18. -- 89d13e5618: Add current and default scopes when refreshing session -- 9ab0572217: Add component data `core.type` marker for `AppRouter` and `FlatRoutes`. -- Updated dependencies - - @backstage/core-plugin-api@1.8.0-next.0 - - @backstage/version-bridge@1.0.7-next.0 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - -## 1.11.0 - -### Minor Changes - -- c9d9bfeca2: URL encode some well known unsafe characters in `RouteResolver` (and therefore `useRouteRef`) - -### Patch Changes - -- 29e4d8b76b: Fixed bug in `AppRouter` to determine the correct `signOutTargetUrl` if `app.baseUrl` contains a `basePath` -- acca17e91a: Wrap entire app in ``, enabling support for using translations outside plugins. -- 1a0616fa10: Add missing resource and template app icons -- 9a1fce352e: Updated dependency `@testing-library/jest-dom` to `^6.0.0`. -- f95af4e540: Updated dependency `@testing-library/dom` to `^9.0.0`. -- f1b349cfba: Fixed a bug in `TranslationApi` implementation where in some cases it wouldn't notify subscribers of language changes. -- Updated dependencies - - @backstage/core-plugin-api@1.7.0 - - @backstage/version-bridge@1.0.6 - - @backstage/config@1.1.1 - - @backstage/types@1.1.1 - -## 1.11.0-next.2 - -### Minor Changes - -- c9d9bfeca2: URL encode some well known unsafe characters in `RouteResolver` (and therefore `useRouteRef`) - -### Patch Changes - -- acca17e91a: Wrap entire app in ``, enabling support for using translations outside plugins. -- Updated dependencies - - @backstage/core-plugin-api@1.7.0-next.1 - - @backstage/config@1.1.1-next.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.1-next.1 - -### Patch Changes - -- 1a0616fa10: Add missing resource and template app icons -- Updated dependencies - - @backstage/core-plugin-api@1.7.0-next.0 - - @backstage/config@1.1.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.7.0-next.0 - - @backstage/config@1.1.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.0 - -### Minor Changes - -- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. -- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. -- 6e30769cc627: Introduced experimental support for internationalization. - -### Patch Changes - -- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. -- 9fe827b380e1: Internal refactor -- 8cec7664e146: Removed `@types/node` dependency -- Updated dependencies - - @backstage/config@1.1.0 - - @backstage/core-plugin-api@1.6.0 - - @backstage/types@1.1.1 - - @backstage/version-bridge@1.0.5 - -## 1.10.0-next.3 - -### Patch Changes - -- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. -- 9fe827b380e1: Internal refactor -- Updated dependencies - - @backstage/config@1.1.0-next.2 - - @backstage/core-plugin-api@1.6.0-next.3 - - @backstage/types@1.1.1-next.0 - - @backstage/version-bridge@1.0.5-next.0 - -## 1.10.0-next.2 - -### Minor Changes - -- 6e30769cc627: Introduced experimental support for internationalization. - -### Patch Changes - -- 8cec7664e146: Removed `@types/node` dependency -- Updated dependencies - - @backstage/core-plugin-api@1.6.0-next.2 - - @backstage/config@1.1.0-next.1 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.10.0-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.1.0-next.0 - - @backstage/core-plugin-api@1.6.0-next.1 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.10.0-next.0 - -### Minor Changes - -- 18619f793c94: Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. -- 18619f793c94: The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.6.0-next.0 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.9.1 - -### Patch Changes - -- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. -- Updated dependencies - - @backstage/core-plugin-api@1.5.3 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.9.1-next.0 - -### Patch Changes - -- 9ae4e7e63836: Fixed a bug that could cause `navigate` analytics events to be misattributed to the plugin mounted on the root route (e.g. the `home` plugin at `/`) when the route that was navigated to wasn't associated with a routable extension. -- Updated dependencies - - @backstage/core-plugin-api@1.5.3 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.9.0 - -### Minor Changes - -- a77ddf7ccd71: add login in popup options to config popup width and height - -### Patch Changes - -- 8174cf4c0edf: Fixing MUI / Material UI references -- Updated dependencies - - @backstage/core-plugin-api@1.5.3 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.8.2-next.1 - -### Patch Changes - -- 8174cf4c0edf: Fixing MUI / Material UI references -- Updated dependencies - - @backstage/core-plugin-api@1.5.3-next.1 - - @backstage/config@1.0.8 - - @backstage/types@1.1.0 - - @backstage/version-bridge@1.0.4 - -## 1.8.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.3-next.0 - - @backstage/config@1.0.8 - -## 1.8.1 - -### Patch Changes - -- 12adfbc8fe2d: Fixed a bug that prevented accurate plugin and route data from being applied to `navigate` analytics events when users visited pages constructed with ``, ``, and similar components that are used to gather one or more routable extensions under a given path. -- ac677bc30ae0: Expose discovery.endpoints configuration to use FrontendHostDiscovery -- 74b216ee4e50: Add `PropsWithChildren` to usages of `ComponentType`, in preparation for React 18 where the children are no longer implicit. -- Updated dependencies - - @backstage/core-plugin-api@1.5.2 - - @backstage/types@1.1.0 - - @backstage/config@1.0.8 - - @backstage/version-bridge@1.0.4 - -## 1.8.1-next.0 - -### Patch Changes - -- 74b216ee4e50: Add `PropsWithChildren` to usages of `ComponentType`, in preparation for React 18 where the children are no longer implicit. -- Updated dependencies - - @backstage/core-plugin-api@1.5.2-next.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4 - -## 1.8.0 - -### Minor Changes - -- c89437db899: The analytics' `navigate` event will now include the route parameters as attributes of the navigate event - -### Patch Changes - -- b645d70034a: Fixed a bug in the Azure auth provider which prevented getting access tokens with multiple scopes for one resource -- 42d817e76ab: Added `FrontendHostDiscovery` for config driven discovery implementation -- Updated dependencies - - @backstage/config@1.0.7 - - @backstage/core-plugin-api@1.5.1 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4 - -## 1.8.0-next.1 - -### Minor Changes - -- c89437db899: The analytics' `navigate` event will now include the route parameters as attributes of the navigate event - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.1 - - @backstage/config@1.0.7 - -## 1.7.1-next.0 - -### Patch Changes - -- 42d817e76ab: Added `FrontendHostDiscovery` for config driven discovery implementation -- Updated dependencies - - @backstage/core-plugin-api@1.5.1 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4 - -## 1.7.0 - -### Minor Changes - -- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window. -- c15e0cedbe1: The `AuthConnector` interface now supports specifying a set of scopes when - refreshing a session. The `DefaultAuthConnector` implementation passes the - `scope` query parameter to the auth-backend plugin appropriately. The - `RefreshingAuthSessionManager` passes any scopes in its `GetSessionRequest` - appropriately. - -### Patch Changes - -- 1e4f5e91b8e: Bump `zod` and `zod-to-json-schema` dependencies. -- e0c6e8b9c3c: Update peer dependencies -- Updated dependencies - - @backstage/core-plugin-api@1.5.1 - - @backstage/version-bridge@1.0.4 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - -## 1.7.0-next.3 - -### Minor Changes - -- c15e0cedbe1: The `AuthConnector` interface now supports specifying a set of scopes when - refreshing a session. The `DefaultAuthConnector` implementation passes the - `scope` query parameter to the auth-backend plugin appropriately. The - `RefreshingAuthSessionManager` passes any scopes in its `GetSessionRequest` - appropriately. - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.7 - - @backstage/core-plugin-api@1.5.1-next.1 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4-next.0 - -## 1.7.0-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.1-next.1 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.4-next.0 - -## 1.7.0-next.1 - -### Patch Changes - -- 1e4f5e91b8e: Bump `zod` and `zod-to-json-schema` dependencies. -- e0c6e8b9c3c: Update peer dependencies -- Updated dependencies - - @backstage/core-plugin-api@1.5.1-next.0 - - @backstage/version-bridge@1.0.4-next.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - -## 1.7.0-next.0 - -### Minor Changes - -- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.6.0 - -### Minor Changes - -- 456eaa8cf83: `OAuth2` now gets ID tokens from a session with the `openid` scope explicitly - requested. - - This should not be considered a breaking change, because spec-compliant OIDC - providers will already be returning ID tokens if and only if the `openid` scope - is granted. - - This change makes the dependence explicit, and removes the burden on - OAuth2-based providers which require an ID token (e.g. this is done by various - default [auth handlers](https://backstage.io/docs/auth/identity-resolver/#authhandler)) to add - `openid` to their default scopes. _That_ could carry another indirect benefit: - by removing `openid` from the default scopes for a provider, grants for - resource-specific access tokens can avoid requesting excess ID token-related - scopes. - -### Patch Changes - -- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.5.0 - - @backstage/config@1.0.7 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.6.0-next.2 - -### Minor Changes - -- 456eaa8cf83: `OAuth2` now gets ID tokens from a session with the `openid` scope explicitly - requested. - - This should not be considered a breaking change, because spec-compliant OIDC - providers will already be returning ID tokens if and only if the `openid` scope - is granted. - - This change makes the dependence explicit, and removes the burden on - OAuth2-based providers which require an ID token (e.g. this is done by various - default [auth handlers](https://backstage.io/docs/auth/identity-resolver/#authhandler)) to add - `openid` to their default scopes. _That_ could carry another indirect benefit: - by removing `openid` from the default scopes for a provider, grants for - resource-specific access tokens can avoid requesting excess ID token-related - scopes. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.5.0-next.2 - - @backstage/config@1.0.7-next.0 - -## 1.5.1-next.1 - -### Patch Changes - -- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.4.1-next.1 - - @backstage/config@1.0.7-next.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.5.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.4.1-next.0 - - @backstage/config@1.0.6 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.5.0 - -### Minor Changes - -- db10b6ef65: Added a Bitbucket Server Auth Provider and added its API to the app defaults - -### Patch Changes - -- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. -- Updated dependencies - - @backstage/core-plugin-api@1.4.0 - - @backstage/config@1.0.6 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.4.1-next.0 - -### Patch Changes - -- dff4d8ddb1: Fixed an issue where an explicit port the frontend base URL could break the app. -- Updated dependencies - - @backstage/config@1.0.6 - - @backstage/core-plugin-api@1.3.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.4.0 - -### Minor Changes - -- bca8e8b393: Allow defining application level feature flags. See [Feature Flags documentation](https://backstage.io/docs/plugins/feature-flags#in-the-application) for reference. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.3.0 - - @backstage/config@1.0.6 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.4.0-next.1 - -### Minor Changes - -- bca8e8b393: Allow defining application level feature flags. See [Feature Flags documentation](https://backstage.io/docs/plugins/feature-flags#in-the-application) for reference. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.3.0-next.1 - - @backstage/config@1.0.6-next.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.3.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.6-next.0 - - @backstage/core-plugin-api@1.2.1-next.0 - - @backstage/types@1.0.2 - - @backstage/version-bridge@1.0.3 - -## 1.3.0 - -### Minor Changes - -- e0d9c9559a: Added a new `AppRouter` component and `app.createRoot()` method that replaces `app.getRouter()` and `app.getProvider()`, which are now deprecated. The new `AppRouter` component is a drop-in replacement for the old router component, while the new `app.createRoot()` method is used instead of the old provider component. - - An old app setup might look like this: - - ```tsx - const app = createApp(/* ... */); - - const AppProvider = app.getProvider(); - const AppRouter = app.getRouter(); - - const routes = ...; - - const App = () => ( - - - - - {routes} - - - ); - - export default App; - ``` - - With these new APIs, the setup now looks like this: - - ```tsx - import { AppRouter } from '@backstage/core-app-api'; - - const app = createApp(/* ... */); - - const routes = ...; - - export default app.createRoot( - <> - - - - {routes} - - , - ); - ``` - - Note that `app.createRoot()` accepts a React element, rather than a component. - -### Patch Changes - -- d3fea4ae0a: Internal fixes to avoid implicit usage of globals -- b05dcd5530: Move the `zod` dependency to a version that does not collide with other libraries -- b4b5b02315: Tweak feature flag registration so that it happens immediately before the first rendering of the app, rather than just after. -- 6870b43dd1: Fix for the automatic rewriting of base URLs. -- 203271b746: Prevent duplicate feature flag components from rendering in the settings when using components -- 3280711113: Updated dependency `msw` to `^0.49.0`. -- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. -- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. -- 8015ff1258: Tweaked wording to use inclusive terminology -- 653d7912ac: Made `WebStorage` notify its subscribers when `localStorage` values change in other tabs/windows -- 63310e3987: Apps will now rewrite the `app.baseUrl` configuration to match the current `location.origin`. The `backend.baseUrl` will also be rewritten in the same way when the `app.baseUrl` and `backend.baseUrl` have matching origins. This will reduce the need for separate frontend builds for different environments. -- Updated dependencies - - @backstage/core-plugin-api@1.2.0 - - @backstage/version-bridge@1.0.3 - - @backstage/types@1.0.2 - - @backstage/config@1.0.5 - -## 1.3.0-next.4 - -### Minor Changes - -- e0d9c9559a: Added a new `AppRouter` component and `app.createRoot()` method that replaces `app.getRouter()` and `app.getProvider()`, which are now deprecated. The new `AppRouter` component is a drop-in replacement for the old router component, while the new `app.createRoot()` method is used instead of the old provider component. - - An old app setup might look like this: - - ```tsx - const app = createApp(/* ... */); - - const AppProvider = app.getProvider(); - const AppRouter = app.getRouter(); - - const routes = ...; - - const App = () => ( - - - - - {routes} - - - ); - - export default App; - ``` - - With these new APIs, the setup now looks like this: - - ```tsx - import { AppRouter } from '@backstage/core-app-api'; - - const app = createApp(/* ... */); - - const routes = ...; - - export default app.createRoot( - <> - - - - {routes} - - , - ); - ``` - - Note that `app.createRoot()` accepts a React element, rather than a component. - -### Patch Changes - -- b05dcd5530: Move the `zod` dependency to a version that does not collide with other libraries -- Updated dependencies - - @backstage/config@1.0.5-next.1 - - @backstage/core-plugin-api@1.2.0-next.2 - - @backstage/types@1.0.2-next.1 - - @backstage/version-bridge@1.0.3-next.0 - -## 1.2.1-next.3 - -### Patch Changes - -- 6870b43dd1: Fix for the automatic rewriting of base URLs. -- 653d7912ac: Made `WebStorage` notify its subscribers when `localStorage` values change in other tabs/windows -- Updated dependencies - - @backstage/config@1.0.5-next.1 - - @backstage/core-plugin-api@1.2.0-next.2 - - @backstage/types@1.0.2-next.1 - - @backstage/version-bridge@1.0.3-next.0 - -## 1.2.1-next.2 - -### Patch Changes - -- b4b5b02315: Tweak feature flag registration so that it happens immediately before the first rendering of the app, rather than just after. -- 203271b746: Prevent duplicate feature flag components from rendering in the settings when using components -- 8015ff1258: Tweaked wording to use inclusive terminology -- 63310e3987: Apps will now rewrite the `app.baseUrl` configuration to match the current `location.origin`. The `backend.baseUrl` will also be rewritten in the same way when the `app.baseUrl` and `backend.baseUrl` have matching origins. This will reduce the need for separate frontend builds for different environments. -- Updated dependencies - - @backstage/core-plugin-api@1.2.0-next.2 - - @backstage/config@1.0.5-next.1 - - @backstage/types@1.0.2-next.1 - - @backstage/version-bridge@1.0.3-next.0 - -## 1.2.1-next.1 - -### Patch Changes - -- d3fea4ae0a: Internal fixes to avoid implicit usage of globals -- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. -- Updated dependencies - - @backstage/version-bridge@1.0.3-next.0 - - @backstage/core-plugin-api@1.1.1-next.1 - - @backstage/types@1.0.2-next.1 - - @backstage/config@1.0.5-next.1 - -## 1.2.1-next.0 - -### Patch Changes - -- 3280711113: Updated dependency `msw` to `^0.49.0`. -- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.1.1-next.0 - - @backstage/types@1.0.2-next.0 - - @backstage/config@1.0.5-next.0 - - @backstage/version-bridge@1.0.2 - -## 1.2.0 - -### Minor Changes - -- 9b737e5f2e: Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. -- 127fcad26d: Deprecated the `homepage` config as the component that used it - `HomepageTimer` - has been removed and replaced by the `HeaderWorldClock` in the home plugin - -### Patch Changes - -- Updated dependencies - - @backstage/version-bridge@1.0.2 - - @backstage/core-plugin-api@1.1.0 - - @backstage/types@1.0.1 - - @backstage/config@1.0.4 - -## 1.2.0-next.0 - -### Minor Changes - -- 9b737e5f2e: Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. -- 127fcad26d: Deprecated the `homepage` config as the component that used it - `HomepageTimer` - has been removed and replaced by the `HeaderWorldClock` in the home plugin - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.1.0-next.0 - - @backstage/types@1.0.1-next.0 - - @backstage/config@1.0.4-next.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1 - -### Patch Changes - -- 27e6404aba: Fixed a bug where gathered index routes would fail to bind routable extensions. This would typically show up when placing a routable extension in the entity page overview tab. -- Updated dependencies - - @backstage/core-plugin-api@1.0.7 - - @backstage/config@1.0.3 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1-next.2 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.3-next.2 - - @backstage/core-plugin-api@1.0.7-next.2 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1-next.1 - -### Patch Changes - -- 27e6404aba: Fixed a bug where gathered index routes would fail to bind routable extensions. This would typically show up when placing a routable extension in the entity page overview tab. -- Updated dependencies - - @backstage/core-plugin-api@1.0.7-next.1 - - @backstage/config@1.0.3-next.1 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.1-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.3-next.0 - - @backstage/core-plugin-api@1.0.7-next.0 - - @backstage/types@1.0.0 - - @backstage/version-bridge@1.0.1 - -## 1.1.0 - -### Minor Changes - -- a448fea691: Updated the routing system to be compatible with React Router v6 stable. - -### Patch Changes - -- 817f3196f6: Updated React Router dependencies to be peer dependencies. - -- f9ec4e46e3: When using React Router v6 stable, it is now possible for components within the `Route` element tree to have `path` props, although they will be ignored. - -- 7d47def9c4: Removed dependency on `@types/jest`. - -- 744fea158b: Added `getSystemIcons()` function to the `AppContext` available through `useApp` that will pull a list of all the icons that have been registered in the App. - -- 667d917488: Updated dependency `msw` to `^0.47.0`. - -- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. - -- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. - -- 8448b53dd6: Clarify that the `WebStorage` observable returns `JsonValue` items. - -- 70299c99d5: Updated `FlatRoutes` to be compatible with React Router v6 stable. - -- e9d40ebf54: If you'd like to send analytics events to multiple implementations, you may now - do so using the `MultipleAnalyticsApi` implementation provided by this package. - - ```tsx - import { MultipleAnalyticsApi } from '@backstage/core-app-api'; - import { - analyticsApiRef, - configApiRef, - storageApiRef, - identityApiRef, - } from '@internal/backstage/core-plugin-api'; - import { CustomAnalyticsApi } from '@internal/analytics'; - import { VendorAnalyticsApi } from '@vendor/analytics'; - - createApiFactory({ - api: analyticsApiRef, - deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, - factory: ({ configApi, identityApi, storageApi }) => - MultipleAnalyticsApi.fromApis([ - VendorAnalyticsApi.fromConfig(configApi, { identityApi }), - CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), - ]), - }), - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.0.6 - - @backstage/config@1.0.2 - -## 1.1.0-next.3 - -### Patch Changes - -- 7d47def9c4: Removed dependency on `@types/jest`. -- Updated dependencies - - @backstage/config@1.0.2-next.0 - - @backstage/core-plugin-api@1.0.6-next.3 - -## 1.1.0-next.2 - -### Patch Changes - -- f9ec4e46e3: When using React Router v6 stable, it is now possible for components within the `Route` element tree to have `path` props, although they will be ignored. - -- 667d917488: Updated dependency `msw` to `^0.47.0`. - -- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. - -- e9d40ebf54: If you'd like to send analytics events to multiple implementations, you may now - do so using the `MultipleAnalyticsApi` implementation provided by this package. - - ```tsx - import { MultipleAnalyticsApi } from '@backstage/core-app-api'; - import { - analyticsApiRef, - configApiRef, - storageApiRef, - identityApiRef, - } from '@internal/backstage/core-plugin-api'; - import { CustomAnalyticsApi } from '@internal/analytics'; - import { VendorAnalyticsApi } from '@vendor/analytics'; - - createApiFactory({ - api: analyticsApiRef, - deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, - factory: ({ configApi, identityApi, storageApi }) => - MultipleAnalyticsApi.fromApis([ - VendorAnalyticsApi.fromConfig(configApi, { identityApi }), - CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), - ]), - }), - ``` - -- Updated dependencies - - @backstage/core-plugin-api@1.0.6-next.2 - -## 1.1.0-next.1 - -### Minor Changes - -- a448fea691: Updated the routing system to be compatible with React Router v6 stable. - -### Patch Changes - -- 817f3196f6: Updated React Router dependencies to be peer dependencies. -- 70299c99d5: Updated `FlatRoutes` to be compatible with React Router v6 stable. -- Updated dependencies - - @backstage/core-plugin-api@1.0.6-next.1 - -## 1.0.6-next.0 - -### Patch Changes - -- 744fea158b: Added `getSystemIcons()` function to the `AppContext` available through `useApp` that will pull a list of all the icons that have been registered in the App. -- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.6-next.0 - -## 1.0.5 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.0.5 - -## 1.0.5-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.0.5-next.0 - -## 1.0.4 - -### Patch Changes - -- 881fc75a75: Internal tweak removing usage of explicit type parameters for the `BackstagePlugin` type. -- 8fe2357101: The `signOut` method of the `IdentityApi` will now navigate the user back to the base URL of the app as indicated by the `app.baseUrl` config. -- a70869e775: Updated dependency `msw` to `^0.43.0`. -- 8006d0f9bf: Updated dependency `msw` to `^0.44.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.4 - -## 1.0.4-next.1 - -### Patch Changes - -- 881fc75a75: Internal tweak removing usage of explicit type parameters for the `BackstagePlugin` type. -- a70869e775: Updated dependency `msw` to `^0.43.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.4-next.0 - -## 1.0.4-next.0 - -### Patch Changes - -- 8fe2357101: The `signOut` method of the `IdentityApi` will now navigate the user back to the base URL of the app as indicated by the `app.baseUrl` config. - -## 1.0.3 - -### Patch Changes - -- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. -- 19781483a2: Handle URLs as the first argument to `fetchApi`, when using the `plugin:` protocol -- Updated dependencies - - @backstage/core-plugin-api@1.0.3 - -## 1.0.3-next.0 - -### Patch Changes - -- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. -- Updated dependencies - - @backstage/core-plugin-api@1.0.3-next.0 - -## 1.0.2 - -### Patch Changes - -- 1fae1f57c9: Fix SAML session schema to no longer require the (deprecated) id, to unbreak session data storage. -- Updated dependencies - - @backstage/core-plugin-api@1.0.2 - - @backstage/config@1.0.1 - -## 1.0.2-next.1 - -### Patch Changes - -- Updated dependencies - - @backstage/config@1.0.1-next.0 - - @backstage/core-plugin-api@1.0.2-next.1 - -## 1.0.2-next.0 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@1.0.2-next.0 - -## 1.0.1 - -### Patch Changes - -- 7c7919777e: build(deps-dev): bump `@testing-library/react-hooks` from 7.0.2 to 8.0.0 -- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 -- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. -- a7bb762dab: fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix -- 230ad0826f: Bump to using `@types/node` v16 -- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. -- Updated dependencies - - @backstage/core-plugin-api@1.0.1 - - @backstage/version-bridge@1.0.1 - -## 1.0.1-next.1 - -### Patch Changes - -- 24254fd433: build(deps): bump `@testing-library/user-event` from 13.5.0 to 14.0.0 -- 3ff2bfb66e: Refactored the route collection logic to prepare for future changes and avoid duplicate element tree traversal for the analytics context. -- 230ad0826f: Bump to using `@types/node` v16 -- Updated dependencies - - @backstage/core-plugin-api@1.0.1-next.0 - -## 1.0.1-next.0 - -### Patch Changes - -- a7bb762dab: fixed empty body issue for POST requests using FetchAPI with 'plugin://' prefix -- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. - -## 1.0.0 - -### Major Changes - -- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). - -### Patch Changes - -- a422d7ce5e: chore(deps): bump `@testing-library/react` from 11.2.6 to 12.1.3 -- f24ef7864e: Minor typo fixes -- Updated dependencies - - @backstage/core-plugin-api@1.0.0 - - @backstage/version-bridge@1.0.0 - - @backstage/config@1.0.0 - - @backstage/types@1.0.0 - -## 0.6.0 - -### Minor Changes - -- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use \`observe# @backstage/core-app-api. -- f3cce3dcf7: **BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. -- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at . -- dbf84eee55: **BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@0.8.0 - -## 0.5.4 - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@0.7.0 - -## 0.5.3 - -### Patch Changes - -- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 -- c77c5c7eb6: Added `backstage.role` to `package.json` -- Updated dependencies - - @backstage/core-plugin-api@0.6.1 - - @backstage/config@0.1.14 - - @backstage/types@0.1.2 - - @backstage/version-bridge@0.1.2 - -## 0.5.2 - -### Patch Changes - -- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`. - - > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change. - -## 0.5.2-next.0 - -### Patch Changes - -- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`. - - > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change. - -## 0.5.1 - -### Patch Changes - -- f959c22787: Asynchronous methods on the identity API can now reliably be called at any time, including early in the bootstrap process or prior to successful sign-in. - - Previously in such situations, a `Tried to access IdentityApi before app was loaded` error would be thrown. Now, those methods will wait and resolve eventually (as soon as a concrete identity API is provided). - -## 0.5.0 - -### Minor Changes - -- ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback. - -### Patch Changes - -- fb565073ec: Add an `allowUrl` callback option to `FetchMiddlewares.injectIdentityAuth` -- f050eec2c0: Added validation during the application startup that detects if there are any plugins present that have not had their required external routes bound. Failing the validation will cause a hard crash as it is a programmer error. It lets you detect early on that there are dangling routes, rather than having them cause an error later on. -- Updated dependencies - - @backstage/core-plugin-api@0.6.0 - - @backstage/config@0.1.13 - -## 0.5.0-next.0 - -### Minor Changes - -- ceebe25391: Removed deprecated `SignInResult` type, which was replaced with the new `onSignInSuccess` callback. - -### Patch Changes - -- Updated dependencies - - @backstage/core-plugin-api@0.6.0-next.0 - - @backstage/config@0.1.13-next.0 - -## 0.4.0 - -### Minor Changes - -- e2eb92c109: Removed previously deprecated `ApiRegistry` export. - -### Patch Changes - -- 34442cd5cf: Fixed an issue where valid SAML and GitHub sessions would be considered invalid and not be stored. - - Deprecated the `SamlSession` and `GithubSession` types. - -- 784d8078ab: Removed direct and transitive Material UI dependencies. - -- Updated dependencies - - @backstage/config@0.1.12 - - @backstage/core-plugin-api@0.5.0 - -## 0.3.1 - -### Patch Changes - -- 4ce51ab0f1: Internal refactor of the `react-use` imports to use `react-use/lib/*` instead. -- Updated dependencies - - @backstage/core-plugin-api@0.4.1 - - @backstage/core-components@0.8.3 - -## 0.3.0 - -### Minor Changes - -- a195284c7b: Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`. -- b3605da81c: - Removed deprecated definition `createApp` from `@backstage/core-app-api` which has been replaced by `@backstage/app-defaults#createApp` - - Removed deprecated type `BackstagePluginWithAnyOutput` - - Removed deprecated constructors for `GithubAuth`, `OAuth2`, and `SamlAuth` as the `create` method should be used instead -- 68f8b10ccd: - Removed deprecation configuration option `theme` from `AppTheme` of the `AppThemeApi` - - Removed reference to `theme` in the `app-defaults` default `AppTheme` - - Removed logic in `AppThemeProvider` that creates `ThemeProvider` from `appTheme.theme` - -### Patch Changes - -- 7927005152: Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available. -- 518ddc00bc: Schema-validate local storage cached session info on load -- Updated dependencies - - @backstage/app-defaults@0.1.3 - - @backstage/core-plugin-api@0.4.0 - - @backstage/core-components@0.8.2 - -## 0.2.1 - -### Patch Changes - -- c11ce4f552: Deprecated `Auth0Auth`, pointing to using `OAuth2` directly instead. -- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`. -- Updated dependencies - - @backstage/core-plugin-api@0.3.1 - - @backstage/core-components@0.8.1 - -## 0.2.0 - -### Minor Changes - -- a036b65c2f: **BREAKING CHANGE** - - The app `SignInPage` component has been updated to switch out the `onResult` callback for a new `onSignInSuccess` callback. This is an immediate breaking change without any deprecation period, as it was deemed to be the way of making this change that had the lowest impact. - - The new `onSignInSuccess` callback directly accepts an implementation of an `IdentityApi`, rather than a `SignInResult`. The `SignInPage` from `@backstage/core-component` has been updated to fit this new API, and as long as you pass on `props` directly you should not see any breakage. - - However, if you implement your own custom `SignInPage`, then this will be a breaking change and you need to migrate over to using the new callback. While doing so you can take advantage of the `UserIdentity.fromLegacy` helper from `@backstage/core-components` to make the migration simpler by still using the `SignInResult` type. This helper is also deprecated though and is only provided for immediate migration. Long-term it will be necessary to build the `IdentityApi` using for example `UserIdentity.create` instead. - - The following is an example of how you can migrate existing usage immediately using `UserIdentity.fromLegacy`: - - ```ts - onResult(signInResult); - // becomes - onSignInSuccess(UserIdentity.fromLegacy(signInResult)); - ``` - - The following is an example of how implement the new `onSignInSuccess` callback of the `SignInPage` using `UserIdentity.create`: - - ```ts - const identityResponse = await authApi.getBackstageIdentity(); - // Profile is optional and will be removed, but allows the - // synchronous getProfile method of the IdentityApi to be used. - const profile = await authApi.getProfile(); - onSignInSuccess( - UserIdentity.create({ - identity: identityResponse.identity, - authApi, - profile, - }), - ); - ``` - -### Patch Changes - -- cd450844f6: Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. -- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly -- Updated dependencies - - @backstage/core-components@0.8.0 - - @backstage/core-plugin-api@0.3.0 - - @backstage/app-defaults@0.1.2 - - @backstage/version-bridge@0.1.1 - -## 0.1.24 - -### Patch Changes - -- 0e7f256034: Fixed a bug where `useRouteRef` would fail in situations where relative navigation was needed and the app was is mounted on a sub-path. This would typically show up as a failure to navigate to a tab on an entity page. -- Updated dependencies - - @backstage/core-components@0.7.6 - - @backstage/theme@0.2.14 - - @backstage/core-plugin-api@0.2.2 - -## 0.1.23 - -### Patch Changes - -- bab752e2b3: Change default port of backend from 7000 to 7007. - - This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. - - You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: - - backend: - listen: 0.0.0.0:7123 - baseUrl: http://localhost:7123 - - More information can be found here: - -- 000190de69: The `ApiRegistry` from `@backstage/core-app-api` class has been deprecated and will be removed in a future release. To replace it, we have introduced two new helpers that are exported from `@backstage/test-utils`, namely `TestApiProvider` and `TestApiRegistry`. - - These two new helpers are more tailored for writing tests and development setups, as they allow for partial implementations of each of the APIs. - - When migrating existing code it is typically best to prefer usage of `TestApiProvider` when possible, so for example the following code: - - ```tsx - render( - - {...} - - ) - ``` - - Would be migrated to this: - - ```tsx - render( - - {...} - - ) - ``` - - In cases where the `ApiProvider` is used in a more standalone way, for example to reuse a set of APIs across multiple tests, the `TestApiRegistry` can be used instead. Note that the `TestApiRegistry` only has a single static factory method, `.from()`, and it is slightly different from the existing `.from()` method on `ApiRegistry` in that it doesn't require the API pairs to be wrapped in an outer array. - - Usage that looks like this: - - ```ts - const apis = ApiRegistry.with( - identityApiRef, - mockIdentityApi as unknown as IdentityApi, - ).with(configApiRef, new ConfigReader({})); - ``` - - OR like this: - - ```ts - const apis = ApiRegistry.from([ - [identityApiRef, mockIdentityApi as unknown as IdentityApi], - [configApiRef, new ConfigReader({})], - ]); - ``` - - Would be migrated to this: - - ```ts - const apis = TestApiRegistry.from( - [identityApiRef, mockIdentityApi], - [configApiRef, new ConfigReader({})], - ); - ``` - - If your app is still using the `ApiRegistry` to construct the `apis` for `createApp`, we recommend that you move over to use the new method of supplying API factories instead, using `createApiFactory`. - -- Updated dependencies - - @backstage/core-plugin-api@0.2.1 - - @backstage/core-components@0.7.5 - -## 0.1.22 - -### Patch Changes - -- Reverted the `createApp` TypeScript type to match the one before version `0.1.21`, as it was an accidental breaking change. - -## 0.1.21 - -### Patch Changes - -- 0b1de52732: Migrated to using new `ErrorApiError` and `ErrorApiErrorContext` names. - -- ecd1fcb80a: Deprecated the `BackstagePluginWithAnyOutput` type. - -- 32bfbafb0f: Start exporting and marking several types as public to address errors in the API report. - -- 014cbf8cb9: The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement. - - The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`. - - The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or Material UI, as it allows you to avoid those dependencies completely. - -- 475edb5bc5: move the BehaviorSubject init into the constructor - -- Updated dependencies - - @backstage/core-components@0.7.4 - - @backstage/core-plugin-api@0.2.0 - - @backstage/app-defaults@0.1.1 - -## 0.1.20 - -### Patch Changes - -- 78c512ce8f: I have added default icons for the catalog, scaffolder, techdocs, and search. -- 8b4284cd5c: Improve API documentation for @backstage/core-plugin-api -- Updated dependencies - - @backstage/core-components@0.7.3 - - @backstage/theme@0.2.13 - - @backstage/core-plugin-api@0.1.13 - -## 0.1.19 - -### Patch Changes - -- 10615525f3: Switch to use the json and observable types from `@backstage/types` -- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. -- 925a967f36: Replace usage of test-utils-core with test-utils -- 6b615e92c8: Api cleanup, adding `@public` where necessary and tweaking some comments -- Updated dependencies - - @backstage/config@0.1.11 - - @backstage/theme@0.2.12 - - @backstage/core-components@0.7.2 - - @backstage/core-plugin-api@0.1.12 - -## 0.1.18 - -### Patch Changes - -- 202f322927: Atlassian auth provider - - - AtlassianAuth added to core-app-api - - Atlassian provider added to plugin-auth-backend - - Updated user-settings with Atlassian connection - -- 36e67d2f24: Internal updates to apply more strict checks to throw errors. - -- Updated dependencies - - @backstage/core-components@0.7.1 - - @backstage/core-plugin-api@0.1.11 - -## 0.1.17 - -### Patch Changes - -- 75bc878221: Internal refactor to avoid importing all of `@material-ui/core`. -- Updated dependencies - - @backstage/core-components@0.7.0 - - @backstage/theme@0.2.11 - -## 0.1.16 - -### Patch Changes - -- d9fd798cc8: The Core App API now automatically instruments all route location changes using - the new Analytics API. Each location change triggers a `navigate` event, which - is an analogue of a "pageview" event in traditional web analytics systems. In - addition to the path, these events provide plugin-level metadata via the - analytics context, which can be useful for analyzing plugin usage: - - ```json - { - "action": "navigate", - "subject": "/the-path/navigated/to?with=params#and-hashes", - "context": { - "extension": "App", - "pluginId": "id-of-plugin-that-exported-the-route", - "routeRef": "associated-route-ref-id" - } - } - ``` - - These events can be identified and handled by checking for the action - `navigate` and the extension `App`. - -- 4c3eea7788: Bitbucket Cloud authentication - based on the existing GitHub authentication + changes around BB apis and updated scope. - - - BitbucketAuth added to core-app-api. - - Bitbucket provider added to plugin-auth-backend. - - Cosmetic entry for Bitbucket connection in user-settings Authentication Providers tab. - -- d6ad46eb22: Stop calling connector.removeSession in StaticAuthSessionManager, instead just discarding the - session locally. - -- Updated dependencies - - @backstage/core-components@0.6.1 - - @backstage/core-plugin-api@0.1.10 - -## 0.1.15 - -### Patch Changes - -- 0c4ee1876f: Enables late registration of plugins into the application by updating ApiHolder when additional plugins have been added in. -- Updated dependencies - - @backstage/core-plugin-api@0.1.9 - - @backstage/core-components@0.6.0 - -## 0.1.14 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.5.0 - - @backstage/config@0.1.10 - -## 0.1.13 - -### Patch Changes - -- 671015f132: Switch to using utilities from \`@backstage/version-bridge'. -- bd1981d609: Allow users to specify their own AppThemeProvider -- Updated dependencies - - @backstage/core-components@0.4.2 - - @backstage/core-plugin-api@0.1.8 - -## 0.1.12 - -### Patch Changes - -- 841666a19: Removed deprecated internal functions. -- Updated dependencies - - @backstage/core-components@0.4.1 - - @backstage/config@0.1.9 - - @backstage/core-plugin-api@0.1.7 - -## 0.1.11 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.4.0 - -## 0.1.10 - -### Patch Changes - -- cfcb486aa: Add system icons for the built-in entity types and use them in the entity list of the `catalog-import` plugin. - -- 392b36fa1: Added support for using authenticating via GitHub Apps in addition to GitHub OAuth Apps. It used to be possible to use GitHub Apps, but they did not handle session refresh correctly. - - Note that GitHub Apps handle OAuth scope at the app installation level, meaning that the `scope` parameter for `getAccessToken` has no effect. When calling `getAccessToken` in open source plugins, one should still include the appropriate scope, but also document in the plugin README what scopes are required in the case of GitHub Apps. - - In addition, the `authHandler` and `signInResolver` options have been implemented for the GitHub provider in the auth backend. - -- Updated dependencies - - @backstage/core-components@0.3.3 - - @backstage/config@0.1.8 - -## 0.1.9 - -### Patch Changes - -- 72a31c29a: Add support for additional app origins -- Updated dependencies - - @backstage/config@0.1.7 - - @backstage/core-components@0.3.2 - - @backstage/theme@0.2.10 - -## 0.1.8 - -### Patch Changes - -- 362657623: Add support for serving the app with a base path other than `/`, which is enabled by including the path in `app.baseUrl`. -- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. -- Updated dependencies - - @backstage/core-components@0.3.1 - - @backstage/core-plugin-api@0.1.6 - -## 0.1.7 - -### Patch Changes - -- Updated dependencies - - @backstage/core-components@0.3.0 - - @backstage/config@0.1.6 - - @backstage/core-plugin-api@0.1.5 - -## 0.1.6 - -### Patch Changes - -- 9d40fcb1e: - Bumping `material-ui/core` version to at least `4.12.2` as they made some breaking changes in later versions which broke `Pagination` of the `Table`. - - Switching out `material-table` to `@material-table/core` for support for the later versions of `material-ui/core` - - This causes a minor API change to `@backstage/core-components` as the interface for `Table` re-exports the `prop` from the underlying `Table` components. - - `onChangeRowsPerPage` has been renamed to `onRowsPerPageChange` - - `onChangePage` has been renamed to `onPageChange` - - Migration guide is here: -- Updated dependencies - - @backstage/core-components@0.2.0 - - @backstage/core-plugin-api@0.1.4 - - @backstage/theme@0.2.9 - -## 0.1.5 - -### Patch Changes - -- ea249c6e6: Fix a bug in `FlatRoutes` that prevented outlets from working with the root route, as well as matching root routes too broadly. -- Updated dependencies - - @backstage/core-components@0.1.6 - -## 0.1.4 - -### Patch Changes - -- 62abffee4: Reintroduce export of `defaultConfigLoader`. -- Updated dependencies - - @backstage/core-components@0.1.4 - -## 0.1.3 - -### Patch Changes - -- dc3e7ce68: Introducing new UnhandledErrorForwarder installed by default. For catching unhandled promise rejections, you can override the API to align with general error handling. -- 5f4339b8c: Adding `FeatureFlag` component and treating `FeatureFlags` as first class citizens to composability API -- Updated dependencies - - @backstage/core-plugin-api@0.1.3 - -## 0.1.2 - -### Patch Changes - -- 9bca2a252: Fixes a type bug where supplying all app icons to `createApp` was required, rather than just a partial list. - -- 75b8537ce: This change adds automatic error boundaries around extensions. - - This means that all exposed parts of a plugin are wrapped in a general error boundary component, that is plugin aware. The default design for the error box is borrowed from `@backstage/errors`. To override the default "fallback", one must provide a component named `ErrorBoundaryFallback` to `createApp`, like so: - - ```ts - const app = createApp({ - components: { - ErrorBoundaryFallback: props => { - // a custom fallback component - return ( - <> -

Oops.

-

- The plugin {props.plugin.getId()} failed with{' '} - {props.error.message} -

- - - ); - }, - }, - }); - ``` - - The props here include: - - - `error`. An `Error` object or something that inherits it that represents the error that was thrown from any inner component. - - `resetError`. A callback that will simply attempt to mount the children of the error boundary again. - - `plugin`. A `BackstagePlugin` that can be used to look up info to be presented in the error message. For instance, you may want to keep a map of your internal plugins and team names or slack channels and present these when an error occurs. Typically, you'll do that by getting the plugin ID with `plugin.getId()`. - -- da8cba44f: Deprecate and disable the extension creation methods, which were added to this package by mistake and should only exist within `@backstage/core-plugin-api`. - -- 9bca2a252: Update `createApp` options to allow plugins with unknown output types in order to improve forwards and backwards compatibility. - -- Updated dependencies [e47336ea4] - -- Updated dependencies [75b8537ce] - -- Updated dependencies [da8cba44f] - - @backstage/core-components@0.1.2 - - @backstage/core-plugin-api@0.1.2 - -## 0.1.1 - -### Patch Changes - -- e7c5e4b30: Update installation instructions in README. -- Updated dependencies [031ccd45f] -- Updated dependencies [e7c5e4b30] - - @backstage/core-plugin-api@0.1.1 - - @backstage/core-components@0.1.1 - - @backstage/theme@0.2.8 + - ## @backstage/config-loader@1.9.0-next.1 diff --git a/docs/releases/v1.30.0-next.3-changelog.md b/docs/releases/v1.30.0-next.3-changelog.md new file mode 100644 index 0000000000..4fa86da821 --- /dev/null +++ b/docs/releases/v1.30.0-next.3-changelog.md @@ -0,0 +1,2199 @@ +# Release v1.30.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.3) + +## @backstage/backend-plugin-api@0.8.0-next.2 + +### Minor Changes + +- 7c5f3b0: The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + + We're looking for ways to make it possible to augment services without the need to replace the entire service. + + Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + + ```diff + // @backstage/backend-defaults + + + export const urlReaderFactoriesServiceRef = createServiceRef({ + + id: 'core.urlReader.factories', + + scope: 'plugin', + + multiton: true, + + }); + + ... + + export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + + factories: urlReaderFactoriesServiceRef, + }, + - async factory({ config, logger }) { + + async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, + + factories, + }); + }, + }); + ``` + + With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + + ```ts + // packages/backend/index.ts + import { createServiceFactory } from '@backstage/backend-plugin-api'; + import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; + ... + + backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, + })); + + ... + + ``` + +### Patch Changes + +- 6061061: Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. + +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. + +- 8b13183: Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + + A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + + Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + + ```ts + const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, + }); + ``` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/frontend-plugin-api@0.7.0-next.2 + +### Minor Changes + +- 72754db: **BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + + This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. + +### Patch Changes + +- 210d066: Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-auth-node@0.5.0-next.2 + +### Minor Changes + +- 579afd0: **BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog@1.22.0-next.2 + +### Minor Changes + +- 6925dcb: Introduces the HasSubdomainsCard component that displays the subdomains of a given domain + +### Patch Changes + +- 604a504: The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder@1.24.0-next.2 + +### Minor Changes + +- 3fca643: Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/app-defaults@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/backend-app-api@0.8.1-next.2 + +### Patch Changes + +- 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. +- 80a0737: Added configuration for the `packages` options to config schema +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-common@0.23.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.4.2-next.2 + +### Patch Changes + +- 0d16b52: Add access restrictions to the JWKS external access method config schema +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 7c5f3b0: Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. +- 1d5f298: Avoid excessive numbers of error listeners on cache clients +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-openapi-utils@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/backend-tasks@0.5.28-next.2 + +### Patch Changes + +- ba9abf4: The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.5-next.2 + +### Patch Changes + +- 8b13183: Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. +- 7c5f3b0: Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.27.0-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/config-loader@1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.8-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- 16cf96c: Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.18-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.5-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.12-next.2 + +### Patch Changes + +- 8209449: Added new APIs for testing extensions +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## @backstage/plugin-api-docs@0.11.8-next.2 + +### Patch Changes + +- 4b6d2cb: Updated dependency `@graphiql/react` to `^0.23.0`. +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-app-backend@0.3.72-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + +## @backstage/plugin-app-visualizer@0.1.9-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + +### Patch Changes + +- 3fca643: Added method `listBranchesByRepository` to `BitbucketCloudClient` +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + +## @backstage/plugin-catalog-backend@1.24.1-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-aws@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-azure@0.1.43-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.37-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.40-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-logs@0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-common@1.0.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-graph@0.4.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + +## @backstage/plugin-catalog-node@1.12.5-next.2 + +### Patch Changes + +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-react@1.12.3-next.2 + +### Patch Changes + +- 012e3eb: Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-devtools@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-devtools-backend@0.3.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools-common@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend@0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-github@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-node@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + +## @backstage/plugin-home@0.7.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## @backstage/plugin-kubernetes@0.11.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## @backstage/plugin-kubernetes-backend@0.18.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 8c1aa06: Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. + This was already documented and supported by the plugin. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## @backstage/plugin-kubernetes-common@0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-node@0.1.17-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-react@0.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications-backend@0.3.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-notifications-backend-module-email@0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-notifications-node@0.2.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-org@0.6.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-org-react@0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.47-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + +## @backstage/plugin-permission-common@0.8.1-next.1 + +### Patch Changes + +- df784fe: Add the MetadataResponse type from @backstage/plugin-permission-node, since this + type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.8.1-next.2 + +### Patch Changes + +- df784fe: The MetadataResponse type has been moved to @backstage/plugin-permission-common + to match the recent move of MetadataResponseSerializedRule, and should be + imported from there going forward. To avoid an immediate breaking change, this + type is still re-exported from this package, but is marked as deprecated and + will be removed in a future release. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-react@0.4.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-proxy-backend@0.5.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.1-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 3fca643: Added autocompletion support for resource `branches` +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 382e868: Added test cases for sentry:project:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-common@1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.9-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-react@1.11.0-next.2 + +### Patch Changes + +- 072c00c: Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes +- 04759f2: Fix null check in `isJsonObject` utility function for scaffolder review state component +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search@1.4.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search-backend@1.5.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.16-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-node@1.2.28-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-common@1.2.14-next.1 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-react@1.7.14-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-signals-backend@0.1.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.8-next.2 + +### Patch Changes + +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-backend@1.10.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-techdocs-node@1.12.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-user-settings@0.8.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.100-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-devtools@0.1.17-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## example-app-next@0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-app-visualizer@0.1.9-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## app-next-example-plugin@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + +## example-backend@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-notifications-backend@0.3.4-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/catalog-model@1.5.0 + +## example-backend-legacy@0.2.101-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## e2e-test@0.2.19-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @internal/plugin-todo-list-backend@1.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @internal/plugin-todo-list-common@1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 diff --git a/microsite/data/plugins/azure-storage.yaml b/microsite/data/plugins/azure-storage.yaml index 655a6421ed..6e723c0eeb 100644 --- a/microsite/data/plugins/azure-storage.yaml +++ b/microsite/data/plugins/azure-storage.yaml @@ -4,9 +4,9 @@ author: Deepankumar authorUrl: https://github.com/deepan10 category: Infrastructure description: Explore Azure Storage Blobs in Backstage. -documentation: https://github.com/deepan10/backstage-plugins/tree/main/plugins/azure-storage +documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/azure-storage-explorer/plugins/azure-storage iconUrl: /img/azure-storage-folder.png -npmPackageName: 'backstage-plugin-azure-storage' +npmPackageName: '@backstage-community/plugin-azure-storage-explorer' tags: - Azure - Azure Storage diff --git a/microsite/data/plugins/infracost.yaml b/microsite/data/plugins/infracost.yaml new file mode 100644 index 0000000000..b6ead655d5 --- /dev/null +++ b/microsite/data/plugins/infracost.yaml @@ -0,0 +1,15 @@ +--- +title: VeeCode Infracost +author: VeeCode Platform +authorUrl: https://platform.vee.codes/ +category: FinOps +description: The Infracost plug-in works in conjunction with terraform and provides a graphical representation and cost estimate data for your entity at your respective provider. +documentation: https://platform.vee.codes/plugin/Infracost +iconUrl: https://veecode-platform.github.io/support/imgs/logo_5.svg +npmPackageName: '@veecode-platform/backstage-plugin-infracost' +tags: + - finops + - infracost + - estimate + - terraform +addedDate: '2024-07-25' diff --git a/microsite/data/plugins/tech-insights.yaml b/microsite/data/plugins/tech-insights.yaml index fbf96df3e6..3bd40d576a 100644 --- a/microsite/data/plugins/tech-insights.yaml +++ b/microsite/data/plugins/tech-insights.yaml @@ -6,7 +6,7 @@ category: Discovery description: Visualize, understand and optimize your team's tech health. documentation: https://roadie.io/backstage/plugins/tech-insights/ iconUrl: https://roadie.io/images/logos/tech-insights.png -npmPackageName: '@backstage/plugin-tech-insights' +npmPackageName: '@backstage-community/plugin-tech-insights' tags: - tech-insights - reporting diff --git a/microsite/sidebars.json b/microsite/sidebars.json index d13ebc71c6..ddccb14c91 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -121,7 +121,8 @@ "features/software-templates/writing-custom-step-layouts", "features/software-templates/authorizing-scaffolder-template-details", "features/software-templates/migrating-to-rjsf-v5", - "features/software-templates/migrating-from-v1beta2-to-v1beta3" + "features/software-templates/migrating-from-v1beta2-to-v1beta3", + "features/software-templates/dry-run-testing" ] }, { @@ -296,6 +297,7 @@ "conf/writing", "conf/defining" ], + "Notifications": ["notifications/index"], "Auth and identity": [ "auth/index", { @@ -393,6 +395,7 @@ "backend-system/architecture/plugins", "backend-system/architecture/extension-points", "backend-system/architecture/modules", + "backend-system/architecture/feature-loaders", "backend-system/architecture/naming-patterns" ] }, diff --git a/mkdocs.yml b/mkdocs.yml index 8527d68c28..3b0c7d9caf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md' + - Dry Run Testing: 'features/software-templates/dry-run-testing.md' - Backstage Search: - Overview: 'features/search/README.md' - Getting Started: 'features/search/getting-started.md' diff --git a/package.json b/package.json index 8e7a047d7b..d9b384fb7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.30.0-next.2", + "version": "1.30.0-next.3", "private": true, "repository": { "type": "git", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index ef2ecbce78..410841fec9 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + ## 1.5.10-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 6bed796754..284e832ae5 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.10-next.1", + "version": "1.5.10-next.2", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 13cd327ef1..62a7b18df0 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,13 @@ # app-next-example-plugin +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + ## 0.0.14-next.1 ### Patch Changes diff --git a/packages/app-next-example-plugin/api-report.md b/packages/app-next-example-plugin/api-report.md index c89a7b599b..962b1473a8 100644 --- a/packages/app-next-example-plugin/api-report.md +++ b/packages/app-next-example-plugin/api-report.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { default as React_2 } from 'react'; // @public (undocumented) -const examplePlugin: BackstagePlugin<{}, {}>; +const examplePlugin: BackstagePlugin<{}, {}, {}>; export default examplePlugin; // @public (undocumented) diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index 64be34d516..ad179ebbe5 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.14-next.1", + "version": "0.0.14-next.2", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 7eb9535771..6382efa811 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,48 @@ # example-app-next +## 0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-app-visualizer@0.1.9-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 0.0.14-next.2 ### Patch Changes diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 8a9d97d560..a12371a135 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.14-next.2", + "version": "0.0.14-next.3", "private": true, "repository": { "type": "git", diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 826040a6cb..095c992e18 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -47,9 +47,11 @@ const IndexPage = createPageExtension({ return (
op -
- Page 1 -
+ {page1Link && ( +
+ Page 1 +
+ )}
Home
@@ -82,10 +84,10 @@ const Page1 = createPageExtension({ return (

This is page 1

- Go back + {indexLink && Go back} Page 2 {/* Page 2 */} - Page X + {xLink && Page X}
Sub-page content: @@ -115,7 +117,7 @@ const ExternalPage = createPageExtension({ return (

This is page X

- Go back + {indexLink && Go back}
); }; diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 4bcd5461a7..88e1134800 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,46 @@ # example-app +## 0.2.100-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-devtools@0.1.17-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 0.2.100-next.2 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 855d32fce7..c4c0c18391 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.100-next.2", + "version": "0.2.100-next.3", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index cbbb9e949e..c7c8883302 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/backend-app-api +## 0.8.1-next.2 + +### Patch Changes + +- 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. +- 80a0737: Added configuration for the `packages` options to config schema +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.8.1-next.1 ### Patch Changes diff --git a/packages/backend-app-api/api-report-alpha.md b/packages/backend-app-api/api-report-alpha.md index e4769f4e72..8616eec29a 100644 --- a/packages/backend-app-api/api-report-alpha.md +++ b/packages/backend-app-api/api-report-alpha.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const featureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', + 'singleton', undefined >; diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 7f9b54dd8f..dea309ebb0 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -49,6 +49,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', + 'singleton', undefined >; @@ -80,6 +81,7 @@ export interface Backend { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', + 'singleton', undefined >; @@ -113,6 +115,7 @@ export interface CreateSpecializedBackendOptions { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', + 'singleton', undefined >; @@ -135,6 +138,7 @@ export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', + 'singleton', undefined >; @@ -161,6 +165,7 @@ export class HostDiscovery implements DiscoveryService { export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', + 'singleton', undefined >; @@ -168,6 +173,7 @@ export const httpAuthServiceFactory: ServiceFactoryCompat< export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', + 'singleton', undefined >; @@ -191,6 +197,7 @@ export type IdentityFactoryOptions = { export const identityServiceFactory: ServiceFactoryCompat< IdentityService, 'plugin', + 'singleton', IdentityFactoryOptions >; @@ -203,6 +210,7 @@ export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', + 'singleton', undefined >; @@ -220,6 +228,7 @@ export function loadBackendConfig(options: { export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', + 'singleton', undefined >; @@ -248,6 +257,7 @@ export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', + 'singleton', undefined >; @@ -278,6 +288,7 @@ export interface RootConfigFactoryOptions { export const rootConfigServiceFactory: ServiceFactoryCompat< RootConfigService, 'root', + 'singleton', RootConfigFactoryOptions >; @@ -294,13 +305,14 @@ export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // @public @deprecated export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', + 'singleton', undefined >; @@ -308,6 +320,7 @@ export const rootLifecycleServiceFactory: ServiceFactoryCompat< export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + 'singleton', undefined >; @@ -315,6 +328,7 @@ export const rootLoggerServiceFactory: ServiceFactoryCompat< export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', + 'singleton', undefined >; @@ -322,6 +336,7 @@ export const schedulerServiceFactory: ServiceFactoryCompat< export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, 'plugin', + 'singleton', undefined >; @@ -329,6 +344,7 @@ export const tokenManagerServiceFactory: ServiceFactoryCompat< export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', + 'singleton', undefined >; @@ -336,6 +352,7 @@ export const urlReaderServiceFactory: ServiceFactoryCompat< export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index a23bca447b..e94da62b77 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -287,6 +287,7 @@ export interface Config { } >; }; + packages?: 'all' | { include?: string[]; exclude?: string[] }; }; /** Discovery options. */ diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index adfd2476a2..e809d1c2f0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "0.8.1-next.1", + "version": "0.8.1-next.2", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" @@ -79,7 +79,7 @@ "minimatch": "^9.0.0", "minimist": "^1.2.5", "morgan": "^1.10.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "path-to-regexp": "^6.2.1", "selfsigned": "^2.0.0", diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 3bbef5a4c0..cf8845fcec 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -24,6 +24,7 @@ import { createBackendPlugin, createBackendModule, createExtensionPoint, + createBackendFeatureLoader, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; @@ -105,6 +106,128 @@ describe('BackendInitializer', () => { expect(factory3).not.toHaveBeenCalled(); }); + it('should discover features from feature loader', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + scope: 'plugin', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + + const pluginInit = jest.fn(async () => {}); + const moduleInit = jest.fn(async () => {}); + + const init = new BackendInitializer(baseFactories); + init.add( + createBackendFeatureLoader({ + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: factory1, + }); + yield createServiceFactory({ + service: ref2, + initialization: 'always', + deps: {}, + factory: factory2, + }); + yield createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + init: pluginInit, + }); + }, + }); + yield createBackendModule({ + pluginId: 'test', + moduleId: 'tester', + register(reg) { + reg.registerInit({ + deps: {}, + init: moduleInit, + }); + }, + }); + }, + }), + ); + await init.start(); + + expect(factory1).toHaveBeenCalled(); + expect(factory2).toHaveBeenCalled(); + expect(pluginInit).toHaveBeenCalled(); + expect(moduleInit).toHaveBeenCalled(); + }); + + it('should refuse to override already initialized services through loaded features', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref1, + deps: {}, + factory: () => ({ x: 1 }), + }), + ]); + init.add( + createBackendFeatureLoader({ + deps: { service1: ref1 }, + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: jest.fn(), + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + 'Unable to set service factory with id 1, service has already been instantiated', + ); + }); + + it('should refuse feature loader that depends on a plugin scoped service', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + }); + + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref1, + deps: {}, + factory: () => ({ x: 1 }), + }), + ]); + init.add( + createBackendFeatureLoader({ + // @ts-expect-error + deps: { service1: ref1 }, + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: jest.fn(), + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + /^Feature loaders can only depend on root scoped services, but 'service1' is scoped to 'plugin'. Offending loader is created at '.*'$/, + ); + }); + it('should initialize plugin scoped services with eager initialization', async () => { const ref1 = createServiceRef<{ x: number }>({ id: '1', @@ -219,6 +342,62 @@ describe('BackendInitializer', () => { await init.start(); }); + it('should allow plugins and modules depend on multiton services', async () => { + expect.assertions(2); + + const multiServiceRef = createServiceRef({ + id: 'a', + multiton: true, + }); + const init = new BackendInitializer(baseFactories); + + init.add( + createServiceFactory({ + service: multiServiceRef, + deps: {}, + factory: () => 'x', + }), + ); + init.add( + createServiceFactory({ + service: multiServiceRef, + deps: {}, + factory: () => 'y', + }), + ); + + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { multi: multiServiceRef }, + async init({ multi }) { + expect(multi).toEqual(['x', 'y']); + }, + }); + }, + }), + ); + + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { multi: multiServiceRef }, + async init({ multi }) { + expect(multi).toEqual(['x', 'y']); + }, + }); + }, + }), + ); + + await init.start(); + }); + it('should forward errors when plugins fail to start', async () => { const init = new BackendInitializer([]); init.add( diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ddad94de14..c0da9eda62 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -25,8 +25,14 @@ import { } from '@backstage/backend-plugin-api'; import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication -// eslint-disable-next-line @backstage/no-forbidden-package-imports -import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import type { + InternalBackendFeature, + InternalBackendFeatureLoader, + InternalBackendRegistrations, +} from '../../../backend-plugin-api/src/wiring/types'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; import { ForwardedError, ConflictError } from '@backstage/errors'; import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -44,10 +50,11 @@ export interface BackendRegisterInit { export class BackendInitializer { #startPromise?: Promise; - #features = new Array(); + #registrations = new Array(); #extensionPoints = new Map(); #serviceRegistry: ServiceRegistry; #registeredFeatures = new Array>(); + #registeredFeatureLoaders = new Array(); constructor(defaultApiFactories: ServiceFactory[]) { this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); @@ -101,21 +108,12 @@ export class BackendInitializer { } #addFeature(feature: BackendFeature) { - if (feature.$$type !== '@backstage/BackendFeature') { - throw new Error( - `Failed to add feature, invalid type '${feature.$$type}'`, - ); - } - if (isServiceFactory(feature)) { this.#serviceRegistry.add(feature); - } else if (isInternalBackendFeature(feature)) { - if (feature.version !== 'v1') { - throw new Error( - `Failed to add feature, invalid version '${feature.version}'`, - ); - } - this.#features.push(feature); + } else if (isBackendFeatureLoader(feature)) { + this.#registeredFeatureLoaders.push(feature); + } else if (isBackendRegistrations(feature)) { + this.#registrations.push(feature); } else { throw new Error( `Failed to add feature, invalid feature ${JSON.stringify(feature)}`, @@ -170,14 +168,16 @@ export class BackendInitializer { this.#serviceRegistry.checkForCircularDeps(); } + await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); + // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); const pluginInits = new Map(); const moduleInits = new Map>(); - // Enumerate all features - for (const feature of this.#features) { + // Enumerate all registrations + for (const feature of this.#registrations) { for (const r of feature.getRegistrations()) { const provides = new Set>(); @@ -205,7 +205,7 @@ export class BackendInitializer { consumes: new Set(Object.values(r.init.deps)), init: r.init, }); - } else { + } else if (r.type === 'module') { let modules = moduleInits.get(r.pluginId); if (!modules) { modules = new Map(); @@ -221,6 +221,8 @@ export class BackendInitializer { consumes: new Set(Object.values(r.init.deps)), init: r.init, }); + } else { + throw new Error(`Invalid registration type '${(r as any).type}'`); } } } @@ -383,16 +385,109 @@ export class BackendInitializer { throw new Error('Unexpected plugin lifecycle service implementation'); } + + async #applyBackendFeatureLoaders(loaders: InternalBackendFeatureLoader[]) { + for (const loader of loaders) { + const deps = new Map(); + const missingRefs = new Set(); + + for (const [name, ref] of Object.entries(loader.deps ?? {})) { + if (ref.scope !== 'root') { + throw new Error( + `Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`, + ); + } + const impl = await this.#serviceRegistry.get( + ref as ServiceRef, + 'root', + ); + if (impl) { + deps.set(name, impl); + } else { + missingRefs.add(ref); + } + } + + if (missingRefs.size > 0) { + const missing = Array.from(missingRefs).join(', '); + throw new Error( + `No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`, + ); + } + + const result = await loader + .loader(Object.fromEntries(deps)) + .catch(error => { + throw new ForwardedError( + `Feature loader ${loader.description} failed`, + error, + ); + }); + + let didAddServiceFactory = false; + const newLoaders = new Array(); + + for await (const feature of result) { + if (isBackendFeatureLoader(feature)) { + newLoaders.push(feature); + } else { + didAddServiceFactory ||= isServiceFactory(feature); + this.#addFeature(feature); + } + } + + // Every time we add a new service factory we need to make sure that we don't have circular dependencies + if (didAddServiceFactory) { + this.#serviceRegistry.checkForCircularDeps(); + } + + // Apply loaders recursively, depth-first + if (newLoaders.length > 0) { + await this.#applyBackendFeatureLoaders(newLoaders); + } + } + } } -function isServiceFactory(feature: BackendFeature): feature is ServiceFactory { - return !!(feature as ServiceFactory).service; -} - -function isInternalBackendFeature( +function toInternalBackendFeature( feature: BackendFeature, -): feature is InternalBackendFeature { - return ( - typeof (feature as InternalBackendFeature).getRegistrations === 'function' - ); +): InternalBackendFeature { + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalBackendFeature; + if (internal.version !== 'v1') { + throw new Error( + `Invalid BackendFeature, bad version '${internal.version}'`, + ); + } + return internal; +} + +function isServiceFactory( + feature: BackendFeature, +): feature is InternalServiceFactory { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'service') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'service' in internal; +} + +function isBackendRegistrations( + feature: BackendFeature, +): feature is InternalBackendRegistrations { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'registrations') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'getRegistrations' in internal; +} + +function isBackendFeatureLoader( + feature: BackendFeature, +): feature is InternalBackendFeatureLoader { + return toInternalBackendFeature(feature).featureType === 'loader'; } diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 56d8d611bb..0abd5605ae 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -58,12 +58,24 @@ function createPluginMetadataServiceFactory(pluginId: string) { export class ServiceRegistry { static create(factories: Array): ServiceRegistry { - const registry = new ServiceRegistry(factories); + const factoryMap = new Map(); + for (const factory of factories) { + if (factory.service.multiton) { + const existing = factoryMap.get(factory.service.id) ?? []; + factoryMap.set( + factory.service.id, + existing.concat(toInternalServiceFactory(factory)), + ); + } else { + factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]); + } + } + const registry = new ServiceRegistry(factoryMap); registry.checkForCircularDeps(); return registry; } - readonly #providedFactories: Map; + readonly #providedFactories: Map; readonly #loadedDefaultFactories: Map< Function, Promise @@ -82,10 +94,8 @@ export class ServiceRegistry { readonly #addedFactoryIds = new Set(); readonly #instantiatedFactories = new Set(); - private constructor(factories: Array) { - this.#providedFactories = new Map( - factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), - ); + private constructor(factories: Map) { + this.#providedFactories = factories; this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); } @@ -93,17 +103,17 @@ export class ServiceRegistry { #resolveFactory( ref: ServiceRef, pluginId: string, - ): Promise | undefined { + ): Promise | undefined { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { - return Promise.resolve( + return Promise.resolve([ toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId)), - ); + ]); } let resolvedFactory: - | Promise - | InternalServiceFactory + | Promise + | InternalServiceFactory[] | undefined = this.#providedFactories.get(ref.id); const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; if (!resolvedFactory && !defaultFactory) { @@ -120,15 +130,18 @@ export class ServiceRegistry { ); this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); } - resolvedFactory = loadedFactory.catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the default factory loader threw an error, ${stringifyError( - error, - )}`, - ); - }); + resolvedFactory = loadedFactory.then( + factory => [factory], + error => { + throw new Error( + `Failed to instantiate service '${ + ref.id + }' because the default factory loader threw an error, ${stringifyError( + error, + )}`, + ); + }, + ); } return Promise.resolve(resolvedFactory); @@ -142,6 +155,9 @@ export class ServiceRegistry { if (this.#providedFactories.get(ref.id)) { return false; } + if (ref.multiton) { + return false; + } return !(ref as InternalServiceRef).__defaultFactory; }); @@ -156,13 +172,13 @@ export class ServiceRegistry { checkForCircularDeps(): void { const graph = DependencyGraph.fromIterable( - Array.from(this.#providedFactories).map( - ([serviceId, serviceFactory]) => ({ - value: serviceId, - provides: [serviceId], - consumes: Object.values(serviceFactory.deps).map(d => d.id), - }), - ), + Array.from(this.#providedFactories).map(([serviceId, factories]) => ({ + value: serviceId, + provides: [serviceId], + consumes: factories.flatMap(factory => + Object.values(factory.deps).map(d => d.id), + ), + })), ); const circularDependencies = Array.from(graph.detectCircularDependencies()); @@ -183,27 +199,36 @@ export class ServiceRegistry { ); } - if (this.#addedFactoryIds.has(factoryId)) { - throw new Error( - `Duplicate service implementations provided for ${factoryId}`, - ); - } - if (this.#instantiatedFactories.has(factoryId)) { throw new Error( `Unable to set service factory with id ${factoryId}, service has already been instantiated`, ); } - this.#addedFactoryIds.add(factoryId); - this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); + if (factory.service.multiton) { + const newFactories = ( + this.#providedFactories.get(factoryId) ?? [] + ).concat(toInternalServiceFactory(factory)); + this.#providedFactories.set(factoryId, newFactories); + } else { + if (this.#addedFactoryIds.has(factoryId)) { + throw new Error( + `Duplicate service implementations provided for ${factoryId}`, + ); + } + + this.#addedFactoryIds.add(factoryId); + this.#providedFactories.set(factoryId, [ + toInternalServiceFactory(factory), + ]); + } } async initializeEagerServicesWithScope( scope: 'root' | 'plugin', pluginId: string = 'root', ) { - for (const factory of this.#providedFactories.values()) { + for (const [factory] of this.#providedFactories.values()) { if (factory.service.scope === scope) { // Root-scoped services are eager by default, plugin-scoped are lazy by default if (scope === 'root' && factory.initialization !== 'lazy') { @@ -215,88 +240,112 @@ export class ServiceRegistry { } } - get(ref: ServiceRef, pluginId: string): Promise | undefined { + get( + ref: ServiceRef, + pluginId: string, + ): Promise | undefined { this.#instantiatedFactories.add(ref.id); - return this.#resolveFactory(ref, pluginId)?.then(factory => { - if (factory.service.scope === 'root') { - let existing = this.#rootServiceImplementations.get(factory); - if (!existing) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + const resolvedFactory = this.#resolveFactory(ref, pluginId); - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope !== 'root') { - throw new Error( - `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, - ); + if (!resolvedFactory) { + return ref.multiton + ? (Promise.resolve([]) as + | Promise + | undefined) + : undefined; + } + + return resolvedFactory + .then(factories => { + return Promise.all( + factories.map(factory => { + if (factory.service.scope === 'root') { + let existing = this.#rootServiceImplementations.get(factory); + if (!existing) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); + + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope !== 'root') { + throw new Error( + `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, + ); + } + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + + existing = Promise.all(rootDeps).then(entries => + factory.factory(Object.fromEntries(entries), undefined), + ); + this.#rootServiceImplementations.set(factory, existing); + } + return existing as Promise; } - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - existing = Promise.all(rootDeps).then(entries => - factory.factory(Object.fromEntries(entries), undefined), - ); - this.#rootServiceImplementations.set(factory, existing); - } - return existing as Promise; - } + let implementation = this.#implementations.get(factory); + if (!implementation) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let implementation = this.#implementations.get(factory); - if (!implementation) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope === 'root') { + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope === 'root') { - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - } + implementation = { + context: Promise.all(rootDeps) + .then(entries => + factory.createRootContext?.(Object.fromEntries(entries)), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, + ); + }), + byPlugin: new Map(), + }; - implementation = { - context: Promise.all(rootDeps) - .then(entries => - factory.createRootContext?.(Object.fromEntries(entries)), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, - ); - }), - byPlugin: new Map(), - }; + this.#implementations.set(factory, implementation); + } - this.#implementations.set(factory, implementation); - } + let result = implementation.byPlugin.get(pluginId) as Promise; + if (!result) { + const allDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let result = implementation.byPlugin.get(pluginId) as Promise; - if (!result) { - const allDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + const target = this.get(serviceRef, pluginId)!; + allDeps.push(target.then(impl => [name, impl])); + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - const target = this.get(serviceRef, pluginId)!; - allDeps.push(target.then(impl => [name, impl])); - } - - result = implementation.context - .then(context => - Promise.all(allDeps).then(entries => - factory.factory(Object.fromEntries(entries), context), - ), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, - ); - }); - implementation.byPlugin.set(pluginId, result); - } - - return result; - }); + result = implementation.context + .then(context => + Promise.all(allDeps).then(entries => + factory.factory(Object.fromEntries(entries), context), + ), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, + ); + }); + implementation.byPlugin.set(pluginId, result); + } + return result; + }), + ); + }) + .then(results => (ref.multiton ? results : results[0])); } } diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 2bd24bc8d5..5eadae4c0f 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/backend-common +## 0.23.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.23.4-next.1 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 04353fc04c..ccf44fba0f 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-common", - "version": "0.23.4-next.1", + "version": "0.23.4-next.2", "description": "Common functionality library for Backstage backends", "backstage": { "role": "node-library" @@ -102,7 +102,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "mysql2": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "p-limit": "^3.1.0", "path-to-regexp": "^6.2.1", diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index c779daacd2..4dc7d1a09a 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -18,6 +18,7 @@ import { AuthService, coreServices, createBackendPlugin, + HttpRouterService, ServiceRef, } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; @@ -107,7 +108,10 @@ export function makeLegacyPlugin< return [key, transform(dep)]; } if (key === 'tokenManager') { - return [key, wrapTokenManager(dep as TokenManager, _auth)]; + return [ + key, + wrapTokenManager(dep as TokenManager, _auth as AuthService), + ]; } return [key, dep]; }), @@ -115,7 +119,7 @@ export function makeLegacyPlugin< const router = await createRouter( pluginEnv as TransformedEnv, ); - _router.use(router); + (_router as HttpRouterService).use(router); }, }); }, diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 387c9d197e..44b0a6b9b0 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/backend-defaults +## 0.4.2-next.2 + +### Patch Changes + +- 0d16b52: Add access restrictions to the JWKS external access method config schema +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 7c5f3b0: Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. +- 1d5f298: Avoid excessive numbers of error listeners on cache clients +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.2-next.1 ### Patch Changes diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md index 7df0b394e3..5ceca7f72e 100644 --- a/packages/backend-defaults/api-report-auth.md +++ b/packages/backend-defaults/api-report-auth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md index 0b2d8706df..e8b6f075ab 100644 --- a/packages/backend-defaults/api-report-cache.md +++ b/packages/backend-defaults/api-report-cache.md @@ -28,6 +28,7 @@ export type CacheManagerOptions = { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md index 0d54b43777..8d204db025 100644 --- a/packages/backend-defaults/api-report-database.md +++ b/packages/backend-defaults/api-report-database.md @@ -35,6 +35,7 @@ export type DatabaseManagerOptions = { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md index dbb469f758..1b19a42aa5 100644 --- a/packages/backend-defaults/api-report-discovery.md +++ b/packages/backend-defaults/api-report-discovery.md @@ -11,6 +11,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md index 613e6a4452..a65a6e157d 100644 --- a/packages/backend-defaults/api-report-httpAuth.md +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index a2bf1cd39b..300e75a44d 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -18,6 +18,7 @@ export function createLifecycleMiddleware( export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md index 36adbbf6e7..54ffb77fed 100644 --- a/packages/backend-defaults/api-report-lifecycle.md +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md index c66f886bf4..40ca4e21b4 100644 --- a/packages/backend-defaults/api-report-logger.md +++ b/packages/backend-defaults/api-report-logger.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md index 86b9605606..eea11ed7c9 100644 --- a/packages/backend-defaults/api-report-permissions.md +++ b/packages/backend-defaults/api-report-permissions.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 61f5c9dc9d..2935ad1128 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -28,8 +28,8 @@ export interface RootConfigFactoryOptions { // @public (undocumented) export const rootConfigServiceFactory: (( options?: RootConfigFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md index c69f66e747..c7580cacd6 100644 --- a/packages/backend-defaults/api-report-rootHealth.md +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootHealthServiceFactory: ServiceFactoryCompat< RootHealthService, 'root', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index 36f3aa3ab6..3686f25a12 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -151,8 +151,8 @@ export type RootHttpRouterFactoryOptions = { // @public (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md index faa83706a7..e522471d04 100644 --- a/packages/backend-defaults/api-report-rootLifecycle.md +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md index 075a8b4aa9..3878420192 100644 --- a/packages/backend-defaults/api-report-rootLogger.md +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -14,6 +14,7 @@ import { transport } from 'winston'; export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index a3467b97de..9953518e09 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -21,6 +21,7 @@ export class DefaultSchedulerService { export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index 0980e3cd67..f561c2cbf5 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -22,6 +22,7 @@ import { HarnessIntegration } from '@backstage/integration'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Readable } from 'stream'; import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeOptions } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeResponse } from '@backstage/backend-plugin-api'; @@ -413,6 +414,13 @@ export type ReadUrlResponseFactoryFromStreamOptions = { lastModifiedAt?: Date; }; +// @public +export const urlReaderFactoriesServiceRef: ServiceRef< + ReaderFactory, + 'plugin', + 'multiton' +>; + // @public export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; @@ -429,6 +437,7 @@ export class UrlReaders { export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md index b77878ff34..27f73d06e1 100644 --- a/packages/backend-defaults/api-report-userInfo.md +++ b/packages/backend-defaults/api-report-userInfo.md @@ -10,6 +10,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', + 'singleton', undefined >; diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index ab1e443fc2..1c784bce0c 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -287,6 +287,46 @@ export interface Config { */ subjectPrefix?: string; }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; } >; }; diff --git a/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js new file mode 100644 index 0000000000..efbef1c76f --- /dev/null +++ b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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 + * @returns { Promise } + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.setNullable('next_run_start_at'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropNullable('next_run_start_at'); + }); +}; diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 7114e7090a..cef9641de6 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.4.2-next.1", + "version": "0.4.2-next.2", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" @@ -165,7 +165,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "mysql2": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "p-limit": "^3.1.0", "path-to-regexp": "^6.2.1", diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 89fabfe977..873bcf9648 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -111,23 +111,10 @@ export class CacheManager { return { getClient: (defaultOptions = {}) => { const clientFactory = (options: CacheServiceOptions) => { - const concreteClient = this.getClientWithTtl( + return this.getClientWithTtl( pluginId, options.defaultTtl ?? this.defaultTtl, ); - - // Always provide an error handler to avoid stopping the process. - concreteClient.on('error', (err: Error) => { - // In all cases, just log the error. - this.logger?.error('Failed to create cache client', err); - - // Invoke any custom error handler if provided. - if (typeof this.errorHandler === 'function') { - this.errorHandler(err); - } - }); - - return concreteClient; }; return new DefaultCacheClient( @@ -149,11 +136,17 @@ export class CacheManager { return (pluginId, defaultTtl) => { if (!store) { store = new KeyvRedis(this.connection); + // Always provide an error handler to avoid stopping the process + store.on('error', (err: Error) => { + this.logger?.error('Failed to create redis cache client', err); + this.errorHandler?.(err); + }); } return new Keyv({ namespace: pluginId, ttl: defaultTtl, store, + emitErrors: false, useRedisSets: this.useRedisSets, }); }; @@ -165,10 +158,16 @@ export class CacheManager { return (pluginId, defaultTtl) => { if (!store) { store = new KeyvMemcache(this.connection); + // Always provide an error handler to avoid stopping the process + store.on('error', (err: Error) => { + this.logger?.error('Failed to create memcache cache client', err); + this.errorHandler?.(err); + }); } return new Keyv({ namespace: pluginId, ttl: defaultTtl, + emitErrors: false, store, }); }; @@ -180,6 +179,7 @@ export class CacheManager { new Keyv({ namespace: pluginId, ttl: defaultTtl, + emitErrors: false, store, }); } diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index c5c4c8708a..3e0782d4a3 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -347,6 +347,7 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + expect(parseDuration({ trigger: 'manual' })).toEqual('manual'); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts index 40aa9d521c..f8d06baf56 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -155,6 +155,9 @@ export function parseDuration( if (typeof frequency === 'object' && 'cron' in frequency) { return frequency.cron; } + if (typeof frequency === 'object' && 'trigger' in frequency) { + return frequency.trigger; + } const parsed = Duration.isDuration(frequency) ? frequency diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts index 1cafe00670..5db82f3ece 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts @@ -503,4 +503,32 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is not set for manually-triggered tasks, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'manual', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + const row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.next_run_start_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts index a1c4ec44fd..c5570a8e30 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts @@ -53,8 +53,8 @@ export class TaskWorker { ); let workCheckFrequency = this.workCheckFrequency; - const isCron = !settings?.cadence.startsWith('P'); - if (!isCron) { + const isDuration = settings?.cadence.startsWith('P'); + if (isDuration) { const cadence = Duration.fromISO(settings.cadence); if (cadence < workCheckFrequency) { workCheckFrequency = cadence; @@ -175,7 +175,9 @@ export class TaskWorker { // read it back again. taskSettingsV2Schema.parse(settings); - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let startAt: Knex.Raw | undefined; let nextStartAt: Knex.Raw | undefined; @@ -194,6 +196,9 @@ export class TaskWorker { nextStartAt = this.nextRunAtRaw(time); startAt ||= nextStartAt; + } else if (isManual) { + nextStartAt = this.knex.raw('null'); + startAt ||= nextStartAt; } else { startAt ||= this.knex.fn.now(); nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); @@ -317,7 +322,9 @@ export class TaskWorker { ticket: string, settings: TaskSettingsV2, ): Promise { - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let nextRun: Knex.Raw; if (isCron) { @@ -325,6 +332,8 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); nextRun = this.nextRunAtRaw(time); + } else if (isManual) { + nextRun = this.knex.raw('null'); } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts index 61e6c1d9a8..173c989543 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts @@ -40,6 +40,10 @@ function isValidCronFormat(c: string | undefined): boolean { } } +function isValidTrigger(t: string): boolean { + return t === 'manual'; +} + export const taskSettingsV1Schema = z.object({ version: z.literal(1), initialDelayDuration: z @@ -68,6 +72,11 @@ export const taskSettingsV2Schema = z.object({ cadence: z .string() .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidTrigger, { + message: "Invalid trigger, expecting 'manual'", + }), + ) .or( z.string().refine(isValidOptionalDurationString, { message: 'Invalid duration, expecting ISO Period', diff --git a/packages/backend-defaults/src/entrypoints/urlReader/index.ts b/packages/backend-defaults/src/entrypoints/urlReader/index.ts index cf927ea031..05271966c9 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/index.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/index.ts @@ -15,4 +15,7 @@ */ export * from './lib'; -export { urlReaderServiceFactory } from './urlReaderServiceFactory'; +export { + urlReaderServiceFactory, + urlReaderFactoriesServiceRef, +} from './urlReaderServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index 810882edf8..2639e99d83 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -14,12 +14,36 @@ * limitations under the License. */ +import { ReaderFactory } from './lib'; import { UrlReaders } from './lib/UrlReaders'; import { coreServices, createServiceFactory, + createServiceRef, } from '@backstage/backend-plugin-api'; +/** + * @public + * A non-singleton reference to URL Reader factory services. + * + * @example + * Creating a service factory implementation for a Custom URL Reader. + * ```ts + * createServiceFactory({ + * service: urlReaderFactoriesServiceRef, + * deps: {}, + * async factory() { + * return CustomUrlReader.factory; + * }, + * }); + * ``` + */ +export const urlReaderFactoriesServiceRef = createServiceRef({ + id: 'core.urlReader.factories', + scope: 'plugin', + multiton: true, +}); + /** * Reading content from external systems. * @@ -34,11 +58,13 @@ export const urlReaderServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, + factories: urlReaderFactoriesServiceRef, }, - async factory({ config, logger }) { + async factory({ config, logger, factories }) { return UrlReaders.default({ config, logger, + factories, }); }, }); diff --git a/packages/backend-defaults/src/migrations.test.ts b/packages/backend-defaults/src/migrations.test.ts new file mode 100644 index 0000000000..579c32e626 --- /dev/null +++ b/packages/backend-defaults/src/migrations.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2024 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/scheduler`; +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(); + + it.each(databases.eachSupportedId())( + '20210928160613_init.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20210928160613_init.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.fn.now(), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: expect.anything(), + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + // This looks odd - you might expect a .toThrow at the end but that + // actually is flaky for some reason specifically on sqlite when + // performing multiple runs in sequence + await expect(knex('backstage_backend_tasks__tasks')).rejects.toEqual( + expect.anything(), + ); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + '20240712211735_nullable_next_run.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20240712211735_nullable_next_run.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: null, + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + await expect( + knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }), + ).rejects.toEqual(expect.anything()); + + await knex.destroy(); + }, + ); +}); diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index ede70bfbca..14111873a1 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/backend-dynamic-feature-service +## 0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.16-next.1 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 39aa60b09b..97dea60170 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -117,6 +117,7 @@ export interface DynamicPluginsFactoryOptions { export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', + 'singleton', undefined >; @@ -127,6 +128,7 @@ export const dynamicPluginsFrontendSchemas: BackendFeatureCompat; export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + 'singleton', undefined >; @@ -147,6 +149,7 @@ export interface DynamicPluginsSchemasService { export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< DynamicPluginsSchemasService, 'root', + 'singleton', DynamicPluginsSchemasOptions >; @@ -154,13 +157,15 @@ export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< export const dynamicPluginsServiceFactory: ServiceFactoryCompat< DynamicPluginProvider, 'root', + 'singleton', DynamicPluginsFactoryOptions >; // @public (undocumented) export const dynamicPluginsServiceRef: ServiceRef< DynamicPluginProvider, - 'root' + 'root', + 'singleton' >; // @public (undocumented) diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 1282d39d6c..d6ac21a3ae 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-dynamic-feature-service", "description": "Backstage dynamic feature service", - "version": "0.2.16-next.1", + "version": "0.2.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index f036362cd2..a601890055 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,46 @@ # example-backend-legacy +## 0.2.101-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.2.101-next.1 ### Patch Changes diff --git a/packages/backend-legacy/package.json b/packages/backend-legacy/package.json index 3f59103f4c..a650924720 100644 --- a/packages/backend-legacy/package.json +++ b/packages/backend-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-legacy", - "version": "0.2.101-next.1", + "version": "0.2.101-next.2", "backstage": { "role": "backend" }, diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 37bb91f625..682036d984 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-openapi-utils +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/errors@1.2.4 + ## 0.1.16-next.1 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index ccf49f2be7..35db691c25 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.1.16-next.1", + "version": "0.1.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 9f5ced6b18..d404d05b33 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,99 @@ # @backstage/backend-plugin-api +## 0.8.0-next.2 + +### Minor Changes + +- 7c5f3b0: The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + + We're looking for ways to make it possible to augment services without the need to replace the entire service. + + Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + + ```diff + // @backstage/backend-defaults + + + export const urlReaderFactoriesServiceRef = createServiceRef({ + + id: 'core.urlReader.factories', + + scope: 'plugin', + + multiton: true, + + }); + + ... + + export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + + factories: urlReaderFactoriesServiceRef, + }, + - async factory({ config, logger }) { + + async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, + + factories, + }); + }, + }); + ``` + + With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + + ```ts + // packages/backend/index.ts + import { createServiceFactory } from '@backstage/backend-plugin-api'; + import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; + ... + + backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, + })); + + ... + + ``` + +### Patch Changes + +- 6061061: Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 8b13183: Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + + A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + + Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + + ```ts + const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, + }); + ``` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.1-next.1 ### Patch Changes diff --git a/packages/backend-plugin-api/api-report-alpha.md b/packages/backend-plugin-api/api-report-alpha.md index 81b378a671..0f4348336a 100644 --- a/packages/backend-plugin-api/api-report-alpha.md +++ b/packages/backend-plugin-api/api-report-alpha.md @@ -17,7 +17,8 @@ export interface FeatureDiscoveryService { // @alpha export const featureDiscoveryServiceRef: ServiceRef< FeatureDiscoveryService, - 'root' + 'root', + 'singleton' >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index b3c35d3699..319ddf0bc6 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -82,14 +82,12 @@ export interface BackendModuleRegistrationPoints { ): void; // (undocumented) registerInit< - Deps extends { - [name in string]: unknown; + TDeps extends { + [name in string]: ServiceRef | ExtensionPoint; }, >(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -105,14 +103,12 @@ export interface BackendPluginRegistrationPoints { ): void; // (undocumented) registerInit< - Deps extends { - [name in string]: unknown; + TDeps extends { + [name in string]: ServiceRef; }, >(options: { - deps: { - [name in keyof Deps]: ServiceRef; - }; - init(deps: Deps): Promise; + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -189,28 +185,73 @@ export type CacheServiceSetOptions = { // @public export namespace coreServices { - const auth: ServiceRef; - const userInfo: ServiceRef; - const cache: ServiceRef; - const rootConfig: ServiceRef; - const database: ServiceRef; - const discovery: ServiceRef; - const rootHealth: ServiceRef; - const httpAuth: ServiceRef; - const httpRouter: ServiceRef; - const lifecycle: ServiceRef; - const logger: ServiceRef; - const permissions: ServiceRef; - const pluginMetadata: ServiceRef; - const rootHttpRouter: ServiceRef; - const rootLifecycle: ServiceRef; - const rootLogger: ServiceRef; - const scheduler: ServiceRef; + const auth: ServiceRef; + const userInfo: ServiceRef; + const cache: ServiceRef; + const rootConfig: ServiceRef; + const database: ServiceRef; + const discovery: ServiceRef; + const rootHealth: ServiceRef; + const httpAuth: ServiceRef; + const httpRouter: ServiceRef; + const lifecycle: ServiceRef; + const logger: ServiceRef; + const permissions: ServiceRef; + const pluginMetadata: ServiceRef< + PluginMetadataService, + 'plugin', + 'singleton' + >; + const rootHttpRouter: ServiceRef; + const rootLifecycle: ServiceRef; + const rootLogger: ServiceRef; + const scheduler: ServiceRef; const // @deprecated - tokenManager: ServiceRef; - const urlReader: ServiceRef; + tokenManager: ServiceRef; + const urlReader: ServiceRef; const // @deprecated - identity: ServiceRef; + identity: ServiceRef; +} + +// @public +export function createBackendFeatureLoader< + TDeps extends { + [name in string]: unknown; + }, +>(options: CreateBackendFeatureLoaderOptions): BackendFeature; + +// @public +export interface CreateBackendFeatureLoaderOptions< + TDeps extends { + [name in string]: unknown; + }, +> { + // (undocumented) + deps?: { + [name in keyof TDeps]: ServiceRef; + }; + // (undocumented) + loader(deps: TDeps): + | Iterable< + | BackendFeature + | Promise<{ + default: BackendFeature; + }> + > + | Promise< + Iterable< + | BackendFeature + | Promise<{ + default: BackendFeature; + }> + > + > + | AsyncIterable< + | BackendFeature + | { + default: BackendFeature; + } + >; } // @public @@ -251,18 +292,20 @@ export interface CreateExtensionPointOptions { // @public export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -271,12 +314,13 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -284,12 +328,19 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - options: PluginServiceFactoryOptions, -): ServiceFactoryCompat; + options: PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -299,18 +350,34 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => PluginServiceFactoryOptions, -): ServiceFactoryCompat; + ) => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; // @public export interface DatabaseService { @@ -447,16 +514,18 @@ export interface PluginMetadataService { // @public @deprecated (undocumented) export type PluginServiceFactoryConfig< TService, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; // @public (undocumented) export interface PluginServiceFactoryOptions< TService, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { @@ -476,7 +545,7 @@ export interface PluginServiceFactoryOptions< ): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -538,15 +607,17 @@ export interface RootLoggerService extends LoggerService {} // @public @deprecated (undocumented) export type RootServiceFactoryConfig< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; // @public (undocumented) export interface RootServiceFactoryOptions< - TService, + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -558,7 +629,7 @@ export interface RootServiceFactoryOptions< factory(deps: ServiceRefsToInstances): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -607,7 +678,10 @@ export interface SchedulerServiceTaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + | { + trigger: 'manual'; + }; initialDelay?: Duration | HumanDuration; scope?: 'global' | 'local'; timeout: Duration | HumanDuration; @@ -620,7 +694,14 @@ export interface SchedulerServiceTaskScheduleDefinitionConfig { cron: string; } | string - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { + trigger: 'manual'; + }; initialDelay?: string | HumanDuration; scope?: 'global' | 'local'; timeout: string | HumanDuration; @@ -639,21 +720,23 @@ export type SearchResponseFile = UrlReaderServiceSearchResponseFile; export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > extends BackendFeature { // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @deprecated (undocumented) export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { // @deprecated (undocumented) ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } // @public @deprecated @@ -663,9 +746,11 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > = { id: string; scope: TScope; + multiton?: TInstances extends 'multiton' ? true : false; T: TService; $$type: '@backstage/ServiceRef'; }; @@ -674,10 +759,15 @@ export type ServiceRef< export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', -> = ServiceRefOptions; + TInstances extends 'singleton' | 'multiton', +> = ServiceRefOptions; // @public (undocumented) -export interface ServiceRefOptions { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', +> { // (undocumented) defaultFactory?( service: ServiceRef, @@ -689,6 +779,8 @@ export interface ServiceRefOptions { // (undocumented) id: string; // (undocumented) + multiton?: TInstances extends 'multiton' ? true : false; + // (undocumented) scope?: TScope; } diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 267fdf6a01..61f8b9353d 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "0.7.1-next.1", + "version": "0.8.0-next.2", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/src/deprecated.ts b/packages/backend-plugin-api/src/deprecated.ts index 17ccccf320..0d02f9ed7e 100644 --- a/packages/backend-plugin-api/src/deprecated.ts +++ b/packages/backend-plugin-api/src/deprecated.ts @@ -28,7 +28,8 @@ import { export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', -> = ServiceRefOptions; + TInstances extends 'singleton' | 'multiton', +> = ServiceRefOptions; /** * @public @@ -36,9 +37,10 @@ export type ServiceRefConfig< */ export type RootServiceFactoryConfig< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; /** * @public @@ -46,7 +48,8 @@ export type RootServiceFactoryConfig< */ export type PluginServiceFactoryConfig< TService, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts index a7ec79a336..a8b84b9084 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts @@ -96,7 +96,8 @@ export interface SchedulerServiceTaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -193,7 +194,12 @@ export interface SchedulerServiceTaskScheduleDefinitionConfig { cron: string; } | string - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -361,14 +367,20 @@ function readDuration(config: Config, key: string): HumanDuration { return readDurationFromConfig(config, { key }); } -function readCronOrDuration( +function readFrequency( config: Config, key: string, -): { cron: string } | HumanDuration { +): { cron: string } | HumanDuration | { trigger: 'manual' } { const value = config.get(key); if (typeof value === 'object' && (value as { cron?: string }).cron) { return value as { cron: string }; } + if ( + typeof value === 'object' && + (value as { trigger?: string }).trigger === 'manual' + ) { + return { trigger: 'manual' }; + } return readDuration(config, key); } @@ -383,7 +395,7 @@ function readCronOrDuration( export function readSchedulerServiceTaskScheduleDefinitionFromConfig( config: Config, ): SchedulerServiceTaskScheduleDefinition { - const frequency = readCronOrDuration(config, 'frequency'); + const frequency = readFrequency(config, 'frequency'); const timeout = readDuration(config, 'timeout'); const initialDelay = config.has('initialDelay') diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts index adf097761b..52322f998b 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createServiceFactory, createServiceRef } from './types'; +import { + InternalServiceFactory, + createServiceFactory, + createServiceRef, +} from './types'; const ref = createServiceRef({ id: 'x' }); const rootDep = createServiceRef({ id: 'y', scope: 'root' }); @@ -36,6 +40,10 @@ describe('createServiceFactory', () => { }, }); expect(metaFactory).toEqual(expect.any(Function)); + expect(metaFactory().$$type).toBe('@backstage/BackendFeature'); + expect((metaFactory() as InternalServiceFactory).featureType).toBe( + 'service', + ); expect(metaFactory().service).toBe(ref); // @ts-expect-error @@ -324,6 +332,24 @@ describe('createServiceFactory', () => { metaFactory(); }); + it('should support old service refs without a multiton field', () => { + const oldPluginDep = pluginDep as Omit; // Old refs don't have a multiton field + const metaFactory = createServiceFactory({ + service: ref, + deps: { + plugin: oldPluginDep, + }, + async factory({ plugin }) { + const plugin1: boolean = plugin; + // @ts-expect-error + const plugin2: number = plugin; + unused(plugin1, plugin2); + return 'x'; + }, + }); + expect(metaFactory).toEqual(expect.any(Function)); + }); + it('should only allow objects as options', () => { // @ts-expect-error const metaFactory = createServiceFactory((_opts: string) => ({ diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index d31350806d..5bca592768 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -24,6 +24,7 @@ import { BackendFeature } from '../../types'; export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > = { id: string; @@ -38,6 +39,8 @@ export type ServiceRef< */ scope: TScope; + multiton?: TInstances extends 'multiton' ? true : false; + /** * Utility for getting the type of the service, using `typeof serviceRef.T`. * Attempting to actually read this value will result in an exception. @@ -51,8 +54,9 @@ export type ServiceRef< export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > extends BackendFeature { - service: ServiceRef; + service: ServiceRef; } /** @@ -62,22 +66,25 @@ export interface ServiceFactory< export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { /** * @deprecated Callable service factories will be removed in a future release, please re-implement the service factory using the available APIs instead. If no options are being passed, you can simply remove the trailing `()`. */ ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } /** @internal */ export interface InternalServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', -> extends ServiceFactory { + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', +> extends ServiceFactory { version: 'v1'; + featureType: 'service'; initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; createRootContext?(deps: { [key in string]: unknown }): Promise; @@ -96,9 +103,14 @@ export interface InternalServiceFactory< export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ -export interface ServiceRefOptions { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', +> { id: string; scope?: TScope; + multiton?: TInstances extends 'multiton' ? true : false; defaultFactory?( service: ServiceRef, ): Promise; @@ -116,8 +128,8 @@ export interface ServiceRefOptions { * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create root scoped services. @@ -125,16 +137,37 @@ export function createServiceRef( * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; +/** + * Creates a new service definition. This overload is used to create plugin scoped services. + * + * @public + */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef { - const { id, scope = 'plugin', defaultFactory } = options; + options: ServiceRefOptions, +): ServiceRef; + +/** + * Creates a new service definition. This overload is used to create root scoped services. + * + * @public + */ +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; +export function createServiceRef< + TService, + TInstances extends 'singleton' | 'multiton', +>( + options: ServiceRefOptions, +): ServiceRef { + const { id, scope = 'plugin', multiton = false, defaultFactory } = options; return { id, scope, + multiton, get T(): TService { throw new Error(`tried to read ServiceRef.T of ${this}`); }, @@ -143,7 +176,7 @@ export function createServiceRef( }, $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, - } as ServiceRef & { + } as ServiceRef & { __defaultFactory?: ( service: ServiceRef, ) => Promise | (() => ServiceFactory)>; @@ -155,12 +188,17 @@ type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, TScope extends 'root' | 'plugin' = 'root' | 'plugin', > = { - [key in keyof T as T[key]['scope'] extends TScope ? key : never]: T[key]['T']; + [key in keyof T as T[key]['scope'] extends TScope + ? key + : never]: T[key]['multiton'] extends true | undefined + ? Array + : T[key]['T']; }; /** @public */ export interface RootServiceFactoryOptions< - TService, + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { @@ -175,7 +213,7 @@ export interface RootServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; } @@ -183,6 +221,7 @@ export interface RootServiceFactoryOptions< /** @public */ export interface PluginServiceFactoryOptions< TService, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, @@ -198,7 +237,7 @@ export interface PluginServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; createRootContext?( deps: ServiceRefsToInstances, @@ -217,12 +256,13 @@ export interface PluginServiceFactoryOptions< */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a root scoped service factory with optional options. * @@ -235,14 +275,15 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory without options. * @@ -251,13 +292,20 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, TOpts extends object | undefined = undefined, >( - options: PluginServiceFactoryOptions, -): ServiceFactoryCompat; + options: PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory with optional options. * @@ -270,6 +318,7 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, @@ -277,25 +326,51 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => PluginServiceFactoryOptions, -): ServiceFactoryCompat; + ) => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext, TOpts extends object | undefined = undefined, >( options: - | RootServiceFactoryOptions - | PluginServiceFactoryOptions - | ((options: TOpts) => RootServiceFactoryOptions) + | RootServiceFactoryOptions + | PluginServiceFactoryOptions | (( options: TOpts, - ) => PluginServiceFactoryOptions) - | (() => RootServiceFactoryOptions) - | (() => PluginServiceFactoryOptions), -): ServiceFactoryCompat { + ) => RootServiceFactoryOptions) + | (( + options: TOpts, + ) => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >) + | (() => RootServiceFactoryOptions) + | (() => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >), +): ServiceFactoryCompat< + TService, + 'root' | 'plugin', + 'singleton' | 'multiton', + TOpts +> { const configCallback = typeof options === 'function' ? options : () => options; const factory = ( @@ -303,18 +378,26 @@ export function createServiceFactory< ): InternalServiceFactory => { const anyConf = configCallback(o!); if (anyConf.service.scope === 'root') { - const c = anyConf as RootServiceFactoryOptions; + const c = anyConf as RootServiceFactoryOptions< + TService, + TInstances, + TImpl, + TDeps + >; return { $$type: '@backstage/BackendFeature', version: 'v1', + featureType: 'service', service: c.service, initialization: c.initialization, deps: c.deps, - factory: async (deps: TDeps) => c.factory(deps), + factory: async (deps: ServiceRefsToInstances) => + c.factory(deps), }; } const c = anyConf as PluginServiceFactoryOptions< TService, + TInstances, TContext, TImpl, TDeps @@ -322,16 +405,19 @@ export function createServiceFactory< return { $$type: '@backstage/BackendFeature', version: 'v1', + featureType: 'service', service: c.service, initialization: c.initialization, ...('createRootContext' in c ? { - createRootContext: async (deps: TDeps) => - c?.createRootContext?.(deps), + createRootContext: async ( + deps: ServiceRefsToInstances, + ) => c?.createRootContext?.(deps), } : {}), deps: c.deps, - factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), + factory: async (deps: ServiceRefsToInstances, ctx: TContext) => + c.factory(deps, ctx), }; }; diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts new file mode 100644 index 0000000000..37fc079551 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts @@ -0,0 +1,195 @@ +/* + * 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 { coreServices, createServiceFactory } from '../services'; +import { InternalServiceFactory } from '../services/system/types'; +import { BackendFeature } from '../types'; +import { createBackendFeatureLoader } from './createBackendFeatureLoader'; +import { createBackendPlugin } from './createBackendPlugin'; +import { InternalBackendFeatureLoader } from './types'; + +describe('createBackendFeatureLoader', () => { + it('should create an empty feature loader', () => { + const result = createBackendFeatureLoader({ + deps: {}, + loader: () => [], + }) as InternalBackendFeatureLoader; + + expect(result.$$type).toEqual('@backstage/BackendFeature'); + expect(result.version).toEqual('v1'); + expect(result.featureType).toEqual('loader'); + expect(result.deps).toEqual({}); + expect(result.loader).toEqual(expect.any(Function)); + expect(result.description).toMatch(/^created at '.*'$/); + }); + + it('should create a feature loader that loads a few features', async () => { + const result = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + loader({ config: _unused }) { + return [ + createBackendPlugin({ + pluginId: 'x', + register() {}, + })(), + createServiceFactory({ + service: coreServices.pluginMetadata, + deps: {}, + factory: () => ({ getId: () => 'fake-id' }), + })(), + // Dynamic import format + Promise.resolve({ + default: createBackendPlugin({ + pluginId: 'y', + register() {}, + })(), + }), + ]; + }, + }) as InternalBackendFeatureLoader; + + expect(result.$$type).toEqual('@backstage/BackendFeature'); + expect(result.version).toEqual('v1'); + expect(result.featureType).toEqual('loader'); + + const results = await result.loader({ config: {} }); + expect(results.length).toBe(3); + const [pluginX, serviceFactory, pluginY] = results; + expect(pluginX.$$type).toBe('@backstage/BackendFeature'); + expect(serviceFactory.$$type).toBe('@backstage/BackendFeature'); + expect(pluginY.$$type).toBe('@backstage/BackendFeature'); + expect((serviceFactory as InternalServiceFactory).service.id).toBe( + coreServices.pluginMetadata.id, + ); + }); + + it('should support multiple output formats', async () => { + const feature = createBackendPlugin({ pluginId: 'x', register() {} })(); + const dynamicFeature = Promise.resolve({ default: feature }); + + async function extractResult(f: BackendFeature) { + const internal = f as InternalBackendFeatureLoader; + return internal.loader({}); + } + + await expect( + extractResult( + createBackendFeatureLoader({ + loader() { + return [feature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async loader() { + return [feature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + *loader() { + yield feature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async *loader() { + yield feature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + loader() { + return [dynamicFeature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async loader() { + return [dynamicFeature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + *loader() { + yield dynamicFeature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async *loader() { + yield dynamicFeature; + }, + }), + ), + ).resolves.toEqual([feature]); + }); + + it('should only allow dependencies on root scoped services', () => { + createBackendFeatureLoader({ + deps: { + rootLogger: coreServices.rootLogger, + }, + loader: () => [], + }); + createBackendFeatureLoader({ + deps: { + // @ts-expect-error + logger: coreServices.logger, + }, + loader: () => [], + }); + createBackendFeatureLoader({ + deps: { + rootLogger: coreServices.rootLogger, + // @ts-expect-error + logger: coreServices.logger, + }, + loader: () => [], + }); + expect('test').toBe('test'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts new file mode 100644 index 0000000000..cfa3e96c65 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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 { ServiceRef } from '../services'; +import { BackendFeature } from '../types'; +import { describeParentCallSite } from './describeParentCallSite'; +import { InternalBackendFeatureLoader } from './types'; + +/** + * @public + * Options for creating a new backend feature loader. + */ +export interface CreateBackendFeatureLoaderOptions< + TDeps extends { [name in string]: unknown }, +> { + deps?: { + [name in keyof TDeps]: ServiceRef; + }; + loader( + deps: TDeps, + ): + | Iterable> + | Promise>> + | AsyncIterable; +} + +/** + * @public + * Creates a new backend feature loader. + */ +export function createBackendFeatureLoader< + TDeps extends { [name in string]: unknown }, +>(options: CreateBackendFeatureLoaderOptions): BackendFeature { + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + featureType: 'loader', + description: `created at '${describeParentCallSite()}'`, + deps: options.deps, + async loader(deps: TDeps) { + const it = await options.loader(deps); + const result = new Array(); + for await (const item of it) { + if ('$$type' in item && item.$$type === '@backstage/BackendFeature') { + result.push(item); + } else if ('default' in item) { + result.push(item.default); + } else { + throw new Error(`Invalid item "${item}"`); + } + } + return result; + }, + } as InternalBackendFeatureLoader; +} diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts similarity index 52% rename from packages/backend-plugin-api/src/wiring/factories.test.ts rename to packages/backend-plugin-api/src/wiring/createBackendModule.test.ts index 6d5c1f39cd..3426d5a90c 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts @@ -14,59 +14,10 @@ * limitations under the License. */ -import { - createBackendModule, - createBackendPlugin, - createExtensionPoint, -} from './factories'; -import { InternalBackendFeature } from './types'; - -describe('createExtensionPoint', () => { - it('should create an ExtensionPoint', () => { - const extensionPoint = createExtensionPoint({ id: 'x' }); - expect(extensionPoint).toBeDefined(); - expect(extensionPoint.id).toBe('x'); - expect(() => extensionPoint.T).not.toThrow(); - expect(String(extensionPoint)).toBe('extensionPoint{x}'); - }); -}); - -describe('createBackendPlugin', () => { - it('should create a BackendPlugin', () => { - const result = createBackendPlugin({ - pluginId: 'x', - register(r) { - r.registerInit({ deps: {}, async init() {} }); - }, - }); - - // legacy form - const legacy = result() as unknown as InternalBackendFeature; - expect(legacy.$$type).toEqual('@backstage/BackendFeature'); - expect(legacy.version).toEqual('v1'); - expect(legacy.getRegistrations).toEqual(expect.any(Function)); - - // new form - const plugin = result as unknown as InternalBackendFeature; - expect(plugin.$$type).toEqual('@backstage/BackendFeature'); - expect(plugin.version).toEqual('v1'); - expect(plugin.getRegistrations).toEqual(expect.any(Function)); - expect(plugin.getRegistrations()).toEqual([ - { - type: 'plugin', - pluginId: 'x', - extensionPoints: [], - init: { - deps: expect.any(Object), - func: expect.any(Function), - }, - }, - ]); - - // @ts-expect-error - expect(plugin({ a: 'a' })).toBeDefined(); - }); -}); +import { createServiceRef } from '../services'; +import { createBackendModule } from './createBackendModule'; +import { createExtensionPoint } from './createExtensionPoint'; +import { InternalBackendRegistrations } from './types'; describe('createBackendModule', () => { it('should create a BackendModule', () => { @@ -79,13 +30,13 @@ describe('createBackendModule', () => { }); // legacy form - const legacy = result() as unknown as InternalBackendFeature; + const legacy = result() as unknown as InternalBackendRegistrations; expect(legacy.$$type).toEqual('@backstage/BackendFeature'); expect(legacy.version).toEqual('v1'); expect(legacy.getRegistrations).toEqual(expect.any(Function)); // new form - const module = result as unknown as InternalBackendFeature; + const module = result as unknown as InternalBackendRegistrations; expect(module.$$type).toEqual('@backstage/BackendFeature'); expect(module.version).toEqual('v1'); expect(module.getRegistrations).toEqual(expect.any(Function)); @@ -105,4 +56,35 @@ describe('createBackendModule', () => { // @ts-expect-error expect(module({ a: 'a' })).toBeDefined(); }); + + it('should be able to depend on all types of dependencies', () => { + const extensionPoint = createExtensionPoint({ id: 'point' }); + const singleServiceRef = createServiceRef({ id: 'single' }); + const multiServiceRef = createServiceRef({ + id: 'multi', + multiton: true, + }); + + const plugin = createBackendModule({ + pluginId: 'x', + moduleId: 'y', + register(r) { + r.registerInit({ + deps: { + point: extensionPoint, + single: singleServiceRef, + multi: multiServiceRef, + }, + async init({ point, single, multi }) { + const a: string = point; + const b: string = single; + const c: string[] = multi; + expect([a, b, c]).toBe('unused'); + }, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); }); diff --git a/packages/backend-plugin-api/src/wiring/createBackendModule.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.ts new file mode 100644 index 0000000000..56f041606e --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.ts @@ -0,0 +1,107 @@ +/* + * 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 { BackendFeatureCompat } from '../types'; +import { + BackendModuleRegistrationPoints, + InternalBackendModuleRegistration, + InternalBackendPluginRegistration, +} from './types'; + +/** + * The configuration options passed to {@link createBackendModule}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateBackendModuleOptions { + /** + * Should exactly match the `id` of the plugin that the module extends. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + + /** + * The ID of this module, used to identify the module and ensure that it is not installed twice. + */ + moduleId: string; + register(reg: BackendModuleRegistrationPoints): void; +} + +/** + * Creates a new backend module for a given plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export function createBackendModule( + options: CreateBackendModuleOptions, +): BackendFeatureCompat { + function getRegistrations() { + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendModuleRegistration['init'] | undefined = undefined; + + options.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error('registerExtensionPoint called after registerInit'); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, + ); + } + + return [ + { + type: 'module', + pluginId: options.pluginId, + moduleId: options.moduleId, + extensionPoints, + init, + }, + ]; + } + + function backendFeatureCompatWrapper() { + return backendFeatureCompatWrapper; + } + + Object.assign(backendFeatureCompatWrapper, { + $$type: '@backstage/BackendFeature' as const, + version: 'v1', + getRegistrations, + }); + + return backendFeatureCompatWrapper as BackendFeatureCompat; +} diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts new file mode 100644 index 0000000000..f4a0877b85 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { createServiceRef } from '../services'; +import { createBackendPlugin } from './createBackendPlugin'; +import { createExtensionPoint } from './createExtensionPoint'; +import { InternalBackendRegistrations } from './types'; + +describe('createBackendPlugin', () => { + it('should create a BackendPlugin', () => { + const result = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, + }); + + // legacy form + const legacy = result() as unknown as InternalBackendRegistrations; + expect(legacy.$$type).toEqual('@backstage/BackendFeature'); + expect(legacy.version).toEqual('v1'); + expect(legacy.getRegistrations).toEqual(expect.any(Function)); + + // new form + const plugin = result as unknown as InternalBackendRegistrations; + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + expect(plugin.version).toEqual('v1'); + expect(plugin.getRegistrations).toEqual(expect.any(Function)); + expect(plugin.getRegistrations()).toEqual([ + { + type: 'plugin', + pluginId: 'x', + extensionPoints: [], + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); + + // @ts-expect-error + expect(plugin({ a: 'a' })).toBeDefined(); + }); + + it('should be able to depend on all compatible dependencies', () => { + const singleServiceRef = createServiceRef({ id: 'single' }); + const multiServiceRef = createServiceRef({ + id: 'multi', + multiton: true, + }); + + const plugin = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ + deps: { + single: singleServiceRef, + multi: multiServiceRef, + }, + async init({ single, multi }) { + const a: string = single; + const b: string[] = multi; + expect([a, b]).toBe('unused'); + }, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); + + it('should not be able to depend on extension points', () => { + const extensionPoint = createExtensionPoint({ id: 'point' }); + + const plugin = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ + deps: { + // @ts-expect-error + point: extensionPoint, + }, + async init() {}, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts new file mode 100644 index 0000000000..f5984ef24b --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts @@ -0,0 +1,100 @@ +/* + * 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 { BackendFeatureCompat } from '../types'; +import { + BackendPluginRegistrationPoints, + InternalBackendPluginRegistration, +} from './types'; + +/** + * The configuration options passed to {@link createBackendPlugin}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateBackendPluginOptions { + /** + * The ID of this plugin. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + register(reg: BackendPluginRegistrationPoints): void; +} + +/** + * Creates a new backend plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export function createBackendPlugin( + options: CreateBackendPluginOptions, +): BackendFeatureCompat { + function getRegistrations() { + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendPluginRegistration['init'] | undefined = undefined; + + options.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error('registerExtensionPoint called after registerInit'); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${options.pluginId}`, + ); + } + + return [ + { + type: 'plugin', + pluginId: options.pluginId, + extensionPoints, + init, + }, + ]; + } + + function backendFeatureCompatWrapper() { + return backendFeatureCompatWrapper; + } + + Object.assign(backendFeatureCompatWrapper, { + $$type: '@backstage/BackendFeature' as const, + version: 'v1', + getRegistrations, + }); + + return backendFeatureCompatWrapper as BackendFeatureCompat; +} diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts new file mode 100644 index 0000000000..825063e845 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts @@ -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 { createExtensionPoint } from './createExtensionPoint'; + +describe('createExtensionPoint', () => { + it('should create an ExtensionPoint', () => { + const extensionPoint = createExtensionPoint({ id: 'x' }); + expect(extensionPoint).toBeDefined(); + expect(extensionPoint.id).toBe('x'); + expect(() => extensionPoint.T).not.toThrow(); + expect(String(extensionPoint)).toBe('extensionPoint{x}'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts new file mode 100644 index 0000000000..9dbae249ea --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts @@ -0,0 +1,58 @@ +/* + * 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 { ExtensionPoint } from './types'; + +/** + * The configuration options passed to {@link createExtensionPoint}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateExtensionPointOptions { + /** + * The ID of this extension point. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + id: string; +} + +/** + * Creates a new backend extension point. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + */ +export function createExtensionPoint( + options: CreateExtensionPointOptions, +): ExtensionPoint { + return { + id: options.id, + get T(): T { + if (process.env.NODE_ENV === 'test') { + // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken + return null as T; + } + throw new Error(`tried to read ExtensionPoint.T of ${this}`); + }, + toString() { + return `extensionPoint{${options.id}}`; + }, + $$type: '@backstage/ExtensionPoint', + }; +} diff --git a/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts new file mode 100644 index 0000000000..35603e33b0 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +// Single re-export to avoid doing this import in multiple places, but still +// avoid duplicate declarations because this one is a bit tricky. +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts deleted file mode 100644 index 81e2247152..0000000000 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ /dev/null @@ -1,229 +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 { BackendFeatureCompat } from '../types'; -import { - BackendModuleRegistrationPoints, - BackendPluginRegistrationPoints, - ExtensionPoint, - InternalBackendModuleRegistration, - InternalBackendPluginRegistration, -} from './types'; - -/** - * The configuration options passed to {@link createExtensionPoint}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateExtensionPointOptions { - /** - * The ID of this extension point. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - id: string; -} - -/** - * Creates a new backend extension point. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} - */ -export function createExtensionPoint( - options: CreateExtensionPointOptions, -): ExtensionPoint { - return { - id: options.id, - get T(): T { - if (process.env.NODE_ENV === 'test') { - // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken - return null as T; - } - throw new Error(`tried to read ExtensionPoint.T of ${this}`); - }, - toString() { - return `extensionPoint{${options.id}}`; - }, - $$type: '@backstage/ExtensionPoint', - }; -} - -/** - * The configuration options passed to {@link createBackendPlugin}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateBackendPluginOptions { - /** - * The ID of this plugin. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - pluginId: string; - register(reg: BackendPluginRegistrationPoints): void; -} - -/** - * Creates a new backend plugin. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export function createBackendPlugin( - options: CreateBackendPluginOptions, -): BackendFeatureCompat { - function getRegistrations() { - const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = - []; - let init: InternalBackendPluginRegistration['init'] | undefined = undefined; - - options.register({ - registerExtensionPoint(ext, impl) { - if (init) { - throw new Error('registerExtensionPoint called after registerInit'); - } - extensionPoints.push([ext, impl]); - }, - registerInit(regInit) { - if (init) { - throw new Error('registerInit must only be called once'); - } - init = { - deps: regInit.deps, - func: regInit.init, - }; - }, - }); - - if (!init) { - throw new Error( - `registerInit was not called by register in ${options.pluginId}`, - ); - } - - return [ - { - type: 'plugin', - pluginId: options.pluginId, - extensionPoints, - init, - }, - ]; - } - - function backendFeatureCompatWrapper() { - return backendFeatureCompatWrapper; - } - - Object.assign(backendFeatureCompatWrapper, { - $$type: '@backstage/BackendFeature' as const, - version: 'v1', - getRegistrations, - }); - - return backendFeatureCompatWrapper as BackendFeatureCompat; -} - -/** - * The configuration options passed to {@link createBackendModule}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateBackendModuleOptions { - /** - * Should exactly match the `id` of the plugin that the module extends. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - pluginId: string; - - /** - * The ID of this module, used to identify the module and ensure that it is not installed twice. - */ - moduleId: string; - register(reg: BackendModuleRegistrationPoints): void; -} - -/** - * Creates a new backend module for a given plugin. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export function createBackendModule( - options: CreateBackendModuleOptions, -): BackendFeatureCompat { - function getRegistrations() { - const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = - []; - let init: InternalBackendModuleRegistration['init'] | undefined = undefined; - - options.register({ - registerExtensionPoint(ext, impl) { - if (init) { - throw new Error('registerExtensionPoint called after registerInit'); - } - extensionPoints.push([ext, impl]); - }, - registerInit(regInit) { - if (init) { - throw new Error('registerInit must only be called once'); - } - init = { - deps: regInit.deps, - func: regInit.init, - }; - }, - }); - - if (!init) { - throw new Error( - `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, - ); - } - - return [ - { - type: 'module', - pluginId: options.pluginId, - moduleId: options.moduleId, - extensionPoints, - init, - }, - ]; - } - - function backendFeatureCompatWrapper() { - return backendFeatureCompatWrapper; - } - - Object.assign(backendFeatureCompatWrapper, { - $$type: '@backstage/BackendFeature' as const, - version: 'v1', - getRegistrations, - }); - - return backendFeatureCompatWrapper as BackendFeatureCompat; -} diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 5197cec050..95eea6c3d4 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import type { - CreateBackendPluginOptions, - CreateBackendModuleOptions, - CreateExtensionPointOptions, -} from './factories'; +import { type CreateBackendModuleOptions } from './createBackendModule'; +import { type CreateBackendPluginOptions } from './createBackendPlugin'; +import { type CreateExtensionPointOptions } from './createExtensionPoint'; +export { createBackendModule } from './createBackendModule'; +export { createBackendPlugin } from './createBackendPlugin'; +export { createExtensionPoint } from './createExtensionPoint'; export { - createBackendModule, - createBackendPlugin, - createExtensionPoint, -} from './factories'; + createBackendFeatureLoader, + type CreateBackendFeatureLoaderOptions, +} from './createBackendFeatureLoader'; export type { BackendModuleRegistrationPoints, diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 55d43ccb7c..1e8c3910a3 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ServiceRef } from '../services/system/types'; +import { InternalServiceFactory, ServiceRef } from '../services/system/types'; import { BackendFeature } from '../types'; /** @@ -36,6 +36,17 @@ export type ExtensionPoint = { $$type: '@backstage/ExtensionPoint'; }; +/** @ignore */ +type DepsToInstances< + TDeps extends { + [key in string]: ServiceRef | ExtensionPoint; + }, +> = { + [key in keyof TDeps]: TDeps[key] extends ServiceRef + ? Array + : TDeps[key]['T']; +}; + /** * The callbacks passed to the `register` method of a backend plugin. * @@ -46,11 +57,13 @@ export interface BackendPluginRegistrationPoints { ref: ExtensionPoint, impl: TExtensionPoint, ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef; - }; - init(deps: Deps): Promise; + registerInit< + TDeps extends { + [name in string]: ServiceRef; + }, + >(options: { + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -64,17 +77,20 @@ export interface BackendModuleRegistrationPoints { ref: ExtensionPoint, impl: TExtensionPoint, ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; + registerInit< + TDeps extends { + [name in string]: ServiceRef | ExtensionPoint; + }, + >(options: { + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } /** @internal */ -export interface InternalBackendFeature extends BackendFeature { +export interface InternalBackendRegistrations extends BackendFeature { version: 'v1'; + featureType: 'registrations'; getRegistrations(): Array< InternalBackendPluginRegistration | InternalBackendModuleRegistration >; @@ -102,3 +118,20 @@ export interface InternalBackendModuleRegistration { func(deps: Record): Promise; }; } + +/** + * @public + */ +export interface InternalBackendFeatureLoader extends BackendFeature { + version: 'v1'; + featureType: 'loader'; + description: string; + deps: Record>; + loader(deps: Record): Promise; +} + +/** @internal */ +export type InternalBackendFeature = + | InternalBackendRegistrations + | InternalBackendFeatureLoader + | InternalServiceFactory; diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 2313e04748..1a5879f14e 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/backend-tasks +## 0.5.28-next.2 + +### Patch Changes + +- ba9abf4: The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.5.28-next.1 ### Patch Changes diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 0d5ced044a..c0f9da3a01 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -62,7 +62,14 @@ export interface TaskScheduleDefinition { cron: string; } | Duration - | HumanDuration_2; + | HumanDuration_2 + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { + trigger: 'manual'; + }; initialDelay?: Duration | HumanDuration_2; scope?: 'global' | 'local'; timeout: Duration | HumanDuration_2; diff --git a/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js new file mode 100644 index 0000000000..efbef1c76f --- /dev/null +++ b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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 + * @returns { Promise } + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.setNullable('next_run_start_at'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropNullable('next_run_start_at'); + }); +}; diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index a6c88c2be4..3f482b38e6 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.28-next.1", + "version": "0.5.28-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/src/migrations.test.ts b/packages/backend-tasks/src/migrations.test.ts index 82208da975..7583948f71 100644 --- a/packages/backend-tasks/src/migrations.test.ts +++ b/packages/backend-tasks/src/migrations.test.ts @@ -81,4 +81,43 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20240712211735_nullable_next_run.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20240712211735_nullable_next_run.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: null, + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + await expect( + knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }), + ).rejects.toEqual(expect.anything()); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 77965632e9..eca899098f 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -345,6 +345,7 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + expect(parseDuration({ trigger: 'manual' })).toEqual('manual'); }); }); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index b1647f4edf..0ccc77a6c1 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -152,6 +152,9 @@ export function parseDuration( if ('cron' in frequency) { return frequency.cron; } + if ('trigger' in frequency) { + return frequency.trigger; + } const parsed = Duration.isDuration(frequency) ? frequency diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 10ebfde5b4..829d7754f8 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -503,4 +503,32 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is not set for manually-triggered tasks, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'manual', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + const row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.next_run_start_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 57cbf2203e..a50b988522 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -52,8 +52,8 @@ export class TaskWorker { ); let workCheckFrequency = this.workCheckFrequency; - const isCron = !settings?.cadence.startsWith('P'); - if (!isCron) { + const isDuration = settings?.cadence.startsWith('P'); + if (isDuration) { const cadence = Duration.fromISO(settings.cadence); if (cadence < workCheckFrequency) { workCheckFrequency = cadence; @@ -174,7 +174,9 @@ export class TaskWorker { // read it back again. taskSettingsV2Schema.parse(settings); - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let startAt: Knex.Raw | undefined; let nextStartAt: Knex.Raw | undefined; @@ -193,6 +195,9 @@ export class TaskWorker { nextStartAt = this.nextRunAtRaw(time); startAt ||= nextStartAt; + } else if (isManual) { + nextStartAt = this.knex.raw('null'); + startAt ||= nextStartAt; } else { startAt ||= this.knex.fn.now(); nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); @@ -316,7 +321,9 @@ export class TaskWorker { ticket: string, settings: TaskSettingsV2, ): Promise { - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let nextRun: Knex.Raw; if (isCron) { @@ -324,6 +331,8 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); nextRun = this.nextRunAtRaw(time); + } else if (isManual) { + nextRun = this.knex.raw('null'); } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts index e94d06d5c3..85099d82db 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts @@ -32,14 +32,20 @@ function readDuration(config: Config, key: string): HumanDuration { return readDurationFromConfig(config, { key }); } -function readCronOrDuration( +function readFrequency( config: Config, key: string, -): { cron: string } | Duration | HumanDuration { +): { cron: string } | Duration | HumanDuration | { trigger: 'manual' } { const value = config.get(key); if (typeof value === 'object' && (value as { cron?: string }).cron) { return value as { cron: string }; } + if ( + typeof value === 'object' && + (value as { trigger?: string }).trigger === 'manual' + ) { + return { trigger: 'manual' }; + } return readDuration(config, key); } @@ -56,7 +62,7 @@ function readCronOrDuration( export function readTaskScheduleDefinitionFromConfig( config: Config, ): TaskScheduleDefinition { - const frequency = readCronOrDuration(config, 'frequency'); + const frequency = readFrequency(config, 'frequency'); const timeout = readDuration(config, 'timeout'); const initialDelay = config.has('initialDelay') diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index e6a31873be..a0bdbbaa64 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -100,7 +100,12 @@ export interface TaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -373,6 +378,10 @@ function isValidCronFormat(c: string | undefined): boolean { } } +function isValidTrigger(t: string): boolean { + return t === 'manual'; +} + export const taskSettingsV1Schema = z.object({ version: z.literal(1), initialDelayDuration: z @@ -401,6 +410,11 @@ export const taskSettingsV2Schema = z.object({ cadence: z .string() .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidTrigger, { + message: "Invalid trigger, expecting 'manual'", + }), + ) .or( z.string().refine(isValidOptionalDurationString, { message: 'Invalid duration, expecting ISO Period', diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 288bc2fd1d..cbca6bdcab 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/backend-test-utils +## 0.4.5-next.2 + +### Patch Changes + +- 8b13183: Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. +- 7c5f3b0: Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.5-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 1f93573564..584fedb1f5 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -156,7 +156,12 @@ export namespace mockServices { // (undocumented) export namespace auth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + AuthService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -165,7 +170,12 @@ export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + CacheService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -174,7 +184,12 @@ export namespace mockServices { // (undocumented) export namespace database { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + DatabaseService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -185,7 +200,12 @@ export namespace mockServices { // (undocumented) export namespace discovery { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + DiscoveryService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -194,7 +214,12 @@ export namespace mockServices { // (undocumented) export namespace events { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + EventsService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -208,8 +233,8 @@ export namespace mockServices { export namespace httpAuth { const factory: ((options?: { defaultCredentials?: BackstageCredentials; - }) => ServiceFactory) & - ServiceFactory; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -218,7 +243,12 @@ export namespace mockServices { // (undocumented) export namespace httpRouter { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + HttpRouterService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -229,7 +259,12 @@ export namespace mockServices { // (undocumented) export namespace identity { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + IdentityService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -238,7 +273,12 @@ export namespace mockServices { // (undocumented) export namespace lifecycle { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + LifecycleService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -247,7 +287,12 @@ export namespace mockServices { // (undocumented) export namespace logger { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + LoggerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -256,7 +301,12 @@ export namespace mockServices { // (undocumented) export namespace permissions { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + PermissionsService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -271,15 +321,28 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory< + RootConfigService, + 'root', + 'singleton' | 'multiton' + > & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory< + RootConfigService, + 'root', + 'singleton' | 'multiton' + >); } // (undocumented) export namespace rootHealth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + RootHealthService, + 'root', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -290,8 +353,8 @@ export namespace mockServices { const // (undocumented) factory: (( options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory) & - ServiceFactory; + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -300,7 +363,12 @@ export namespace mockServices { // (undocumented) export namespace rootLifecycle { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + RootLifecycleService, + 'root', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -315,10 +383,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory); const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -327,7 +395,12 @@ export namespace mockServices { // (undocumented) export namespace scheduler { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + SchedulerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -338,7 +411,12 @@ export namespace mockServices { // (undocumented) export namespace tokenManager { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + TokenManagerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -347,7 +425,12 @@ export namespace mockServices { // (undocumented) export namespace urlReader { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + UrlReaderService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -358,7 +441,12 @@ export namespace mockServices { ): UserInfoService; // (undocumented) export namespace userInfo { - const factory: ServiceFactoryCompat; + const factory: ServiceFactoryCompat< + UserInfoService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -374,22 +462,34 @@ export function registerMswTestHooks(worker: { }): void; // @public -export class ServiceFactoryTester { - static from( - subject: ServiceFactory, +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', +> { + static from< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ): ServiceFactoryTester; + ): ServiceFactoryTester; // @deprecated get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; - getService( - service: ServiceRef, + ): Promise; + getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetInstances extends 'singleton' | 'multiton' = 'singleton', + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; } // @public diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index bb0786b02c..44373e733f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "0.4.5-next.1", + "version": "0.4.5-next.2", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index aea0d9d270..e8530f89bf 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -43,8 +43,12 @@ export interface ServiceFactoryTesterOptions { * * @public */ -export class ServiceFactoryTester { - readonly #subject: ServiceRef; +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', +> { + readonly #subject: ServiceRef; readonly #registry: ServiceRegistry; /** @@ -54,10 +58,14 @@ export class ServiceFactoryTester { * @param options - Additional options * @returns A new tester instance for the provided subject. */ - static from( - subject: ServiceFactory, + static from< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ) { + ): ServiceFactoryTester { const registry = ServiceRegistry.create([ ...defaultServiceFactories, ...(options?.dependencies ?? []), @@ -67,7 +75,7 @@ export class ServiceFactoryTester { } private constructor( - subject: ServiceRef, + subject: ServiceRef, registry: ServiceRegistry, ) { this.#subject = subject; @@ -81,7 +89,7 @@ export class ServiceFactoryTester { */ async get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { return this.getSubject(...args); } @@ -97,9 +105,10 @@ export class ServiceFactoryTester { */ async getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; - return this.#registry.get(this.#subject, pluginId ?? 'test')!; + const instance = this.#registry.get(this.#subject, pluginId ?? 'test')!; + return instance; } /** @@ -109,10 +118,14 @@ export class ServiceFactoryTester { * * A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used. */ - async getService( - service: ServiceRef, + async getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetInstances extends 'singleton' | 'multiton' = 'singleton', + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; const instance = await this.#registry.get(service, pluginId ?? 'test'); if (instance === undefined) { diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 3d662a8c12..cf98c25e4a 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -36,7 +36,10 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports -import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import { + InternalBackendFeature, + InternalBackendRegistrations, +} from '@backstage/backend-plugin-api/src/wiring/types'; import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter'; /** @public */ @@ -94,7 +97,7 @@ function createPluginsForOrphanModules(features: Array) { const modulePluginIds = new Set(); for (const feature of features) { - if (isInternalBackendFeature(feature)) { + if (isInternalBackendRegistrations(feature)) { const registrations = feature.getRegistrations(); for (const registration of registrations) { if (registration.type === 'plugin') { @@ -137,18 +140,7 @@ function createExtensionPointTestModules( } const registrations = features.flatMap(feature => { - if (feature.$$type !== '@backstage/BackendFeature') { - throw new Error( - `Failed to add feature, invalid type '${feature.$$type}'`, - ); - } - - if (isInternalBackendFeature(feature)) { - if (feature.version !== 'v1') { - throw new Error( - `Failed to add feature, invalid version '${feature.version}'`, - ); - } + if (isInternalBackendRegistrations(feature)) { return feature.getRegistrations(); } return []; @@ -361,10 +353,28 @@ function registerTestHooks() { registerTestHooks(); -function isInternalBackendFeature( +function toInternalBackendFeature( feature: BackendFeature, -): feature is InternalBackendFeature { - return ( - typeof (feature as InternalBackendFeature).getRegistrations === 'function' - ); +): InternalBackendFeature { + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalBackendFeature; + if (internal.version !== 'v1') { + throw new Error( + `Invalid BackendFeature, bad version '${internal.version}'`, + ); + } + return internal; +} + +function isInternalBackendRegistrations( + feature: BackendFeature, +): feature is InternalBackendRegistrations { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'registrations') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'getRegistrations' in internal; } diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 0c58a76dd1..8fbce11752 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,42 @@ # example-backend +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-notifications-backend@0.3.4-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/catalog-model@1.5.0 + ## 0.0.29-next.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 91c63d7553..cafe89a527 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.29-next.1", + "version": "0.0.29-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1174b7bf6d..952f3315c4 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,9 +15,21 @@ */ import { createBackend } from '@backstage/backend-defaults'; +import { createBackendFeatureLoader } from '@backstage/backend-plugin-api'; const backend = createBackend(); +// An example of how to group together and load multiple features. You can also +// access root-scoped services by adding `deps`. +const searchLoader = createBackendFeatureLoader({ + *loader() { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + }, +}); + backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); @@ -36,13 +48,10 @@ backend.add(import('@backstage/plugin-permission-backend/alpha')); backend.add(import('@backstage/plugin-proxy-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); -backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-backstage-openapi'), ); -backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add(searchLoader); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 2d91fa44a5..de941e2502 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -123,6 +123,7 @@ interface DomainEntityV1alpha1 extends Entity { spec: { owner: string; subdomainOf?: string; + type?: string; }; } export { DomainEntityV1alpha1 as DomainEntity }; @@ -454,6 +455,7 @@ interface SystemEntityV1alpha1 extends Entity { spec: { owner: string; domain?: string; + type?: string; }; } export { SystemEntityV1alpha1 as SystemEntity }; diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml index 598f8500aa..02370d9a14 100644 --- a/packages/catalog-model/examples/domains/artists-domain.yaml +++ b/packages/catalog-model/examples/domains/artists-domain.yaml @@ -13,3 +13,4 @@ metadata: spec: owner: team-a subdomainOf: audio + type: product-group diff --git a/packages/catalog-model/examples/domains/playback-domain.yaml b/packages/catalog-model/examples/domains/playback-domain.yaml index 58b5afe56e..e058ebf0a2 100644 --- a/packages/catalog-model/examples/domains/playback-domain.yaml +++ b/packages/catalog-model/examples/domains/playback-domain.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: user:frank.tiernan subdomainOf: audio + type: product-group diff --git a/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml b/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml index 8de3c00880..17fe21ba69 100644 --- a/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml +++ b/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml @@ -8,3 +8,4 @@ metadata: spec: owner: team-a domain: artists + type: service diff --git a/packages/catalog-model/examples/systems/audio-playback-system.yaml b/packages/catalog-model/examples/systems/audio-playback-system.yaml index 7430ae2ff5..f1edb7f17d 100644 --- a/packages/catalog-model/examples/systems/audio-playback-system.yaml +++ b/packages/catalog-model/examples/systems/audio-playback-system.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: team-c domain: playback + type: feature-set diff --git a/packages/catalog-model/examples/systems/podcast-system.yaml b/packages/catalog-model/examples/systems/podcast-system.yaml index 47a2f7ac9f..77f53925c1 100644 --- a/packages/catalog-model/examples/systems/podcast-system.yaml +++ b/packages/catalog-model/examples/systems/podcast-system.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: team-b domain: playback + type: feature-set diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts index 3c1e7f0a6d..61408b7e80 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts @@ -32,6 +32,7 @@ describe('DomainV1alpha1Validator', () => { spec: { owner: 'me', subdomainOf: 'parent-domain', + type: 'domain-type', }, }; }); @@ -84,4 +85,19 @@ describe('DomainV1alpha1Validator', () => { (entity as any).spec.subdomainOf = ''; await expect(validator.check(entity)).rejects.toThrow(/subdomainOf/); }); + + it('accepts missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index 1fe3dfb522..765329abb4 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -33,6 +33,7 @@ export interface DomainEntityV1alpha1 extends Entity { spec: { owner: string; subdomainOf?: string; + type?: string; }; } diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts index fd78633a89..828fe7a648 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts @@ -32,6 +32,7 @@ describe('SystemV1alpha1Validator', () => { spec: { owner: 'me', domain: 'domain', + type: 'system-type', }, }; }); @@ -84,4 +85,19 @@ describe('SystemV1alpha1Validator', () => { (entity as any).spec.domain = ''; await expect(validator.check(entity)).rejects.toThrow(/domain/); }); + + it('accepts missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 88d405ec5b..37ecee8f6c 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -33,6 +33,7 @@ export interface SystemEntityV1alpha1 extends Entity { spec: { owner: string; domain?: string; + type?: string; }; } diff --git a/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json index 7d0d7e25ff..e4a6c29dd1 100644 --- a/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json @@ -12,7 +12,8 @@ }, "spec": { "owner": "artist-relations-team", - "subdomainOf": "audio" + "subdomainOf": "audio", + "type": "product-group" } } ], @@ -45,6 +46,12 @@ "description": "An entity reference to another domain of which the domain is a part.", "examples": ["audio"], "minLength": 1 + }, + "type": { + "type": "string", + "description": "The type of domain. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their catalog hierarchy.", + "examples": ["product-group", "bundle"], + "minLength": 1 } } } diff --git a/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json index 2cdbc37076..f91fe751dc 100644 --- a/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json @@ -12,7 +12,8 @@ }, "spec": { "owner": "artist-relations-team", - "domain": "artists" + "domain": "artists", + "type": "service" } } ], @@ -45,6 +46,12 @@ "description": "An entity reference to the domain that the system belongs to.", "examples": ["artists"], "minLength": 1 + }, + "type": { + "type": "string", + "description": "The type of system. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their catalog hierarchy.", + "examples": ["product", "service", "feature-set"], + "minLength": 1 } } } diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index df526b2b00..887445aac7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/cli +## 0.27.0-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + ## 0.27.0-next.2 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 768851ebc2..ba45b8f514 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.27.0-next.2", + "version": "0.27.0-next.3", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -121,7 +121,7 @@ "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-libs-browser": "^2.2.1", "npm-packlist": "^5.0.0", "ora": "^5.3.0", diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 42cdae24e3..da7b378fbd 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -180,7 +180,7 @@ export async function createConfig( // to remove this eventually! plugins.push( new ProvidePlugin({ - process: 'process/browser', + process: require.resolve('process/browser'), Buffer: ['buffer', 'Buffer'], }), ); diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index a026e7549d..78f2123970 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/config-loader +## 1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.9.0-next.1 ### Minor Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 08589bfca4..a47a17c12f 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.9.0-next.1", + "version": "1.9.0-next.2", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" @@ -49,7 +49,7 @@ "json-schema-traverse": "^1.0.0", "lodash": "^4.17.21", "minimist": "^1.2.5", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "typescript-json-schema": "^0.63.0", "yaml": "^2.0.0" }, diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 587d78242b..34c134f731 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/core-compat-api +## 0.2.8-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- 16cf96c: Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + ## 0.2.8-next.1 ### Patch Changes diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index be80ca50e9..8d177b4dab 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -37,12 +37,24 @@ export function convertLegacyRouteRef( ): SubRouteRef_2; // @public -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: ExternalRouteRef, -): ExternalRouteRef_2; +export function convertLegacyRouteRef( + ref: ExternalRouteRef, +): ExternalRouteRef_2; + +// @public +export function convertLegacyRouteRef( + ref: RouteRef_2, +): RouteRef; + +// @public +export function convertLegacyRouteRef( + ref: SubRouteRef_2, +): SubRouteRef; + +// @public +export function convertLegacyRouteRef( + ref: ExternalRouteRef_2, +): ExternalRouteRef; // @public export function convertLegacyRouteRefs< @@ -75,8 +87,8 @@ export type ToNewRouteRef = ? RouteRef_2 : T extends SubRouteRef ? SubRouteRef_2 - : T extends ExternalRouteRef - ? ExternalRouteRef_2 + : T extends ExternalRouteRef + ? ExternalRouteRef_2 : never; // (No @packageDocumentation comment for this package) diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index a76f7878dd..0434b206d9 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "backstage": { "role": "web-library" }, @@ -44,6 +44,7 @@ "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", + "@backstage/test-utils": "workspace:^", "@oriflame/backstage-plugin-score-card": "^0.8.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^15.0.0" diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 288f3f900b..4f9ba3a836 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -50,7 +50,9 @@ const legacyPluginStore = getOrCreateGlobalSingleton( () => new WeakMap(), ); -function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin { +export function toLegacyPlugin( + plugin: NewBackstagePlugin, +): LegacyBackstagePlugin { let legacy = legacyPluginStore.get(plugin); if (legacy) { return legacy; diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 5544862aee..9c646d5b00 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -14,10 +14,155 @@ * limitations under the License. */ -import React from 'react'; +import { + ApiHolder, + ApiRef, + AppContext, + useApp, +} from '@backstage/core-plugin-api'; +import { + AnyRouteRefParams, + ComponentRef, + ComponentsApi, + CoreErrorBoundaryFallbackProps, + CoreNotFoundErrorPageProps, + CoreProgressProps, + ExternalRouteRef, + IconComponent, + IconsApi, + RouteFunc, + RouteRef, + RouteResolutionApi, + RouteResolutionApiResolveOptions, + SubRouteRef, + componentsApiRef, + coreComponentRefs, + iconsApiRef, + routeResolutionApiRef, +} from '@backstage/frontend-plugin-api'; +import React, { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; +import { toLegacyPlugin } from './BackwardsCompatProvider'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { ApiProvider } from '../../../core-app-api/src/apis/system/ApiProvider'; +import { useVersionedContext } from '@backstage/version-bridge'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { type RouteResolver } from '../../../core-plugin-api/src/routing/useRouteRef'; +import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; + +class CompatComponentsApi implements ComponentsApi { + readonly #Progress: ComponentType; + readonly #NotFoundErrorPage: ComponentType; + readonly #ErrorBoundaryFallback: ComponentType; + + constructor(app: AppContext) { + const components = app.getComponents(); + const ErrorBoundaryFallback = (props: CoreErrorBoundaryFallbackProps) => ( + + ); + this.#Progress = components.Progress; + this.#NotFoundErrorPage = components.NotFoundErrorPage; + this.#ErrorBoundaryFallback = ErrorBoundaryFallback; + } + + getComponent(ref: ComponentRef): ComponentType { + switch (ref.id) { + case coreComponentRefs.progress.id: + return this.#Progress as ComponentType; + case coreComponentRefs.notFoundErrorPage.id: + return this.#NotFoundErrorPage as ComponentType; + case coreComponentRefs.errorBoundaryFallback.id: + return this.#ErrorBoundaryFallback as ComponentType; + default: + throw new Error( + `No backwards compatible component is available for ref '${ref.id}'`, + ); + } + } +} + +class CompatIconsApi implements IconsApi { + readonly #app: AppContext; + + constructor(app: AppContext) { + this.#app = app; + } + + getIcon(key: string): IconComponent | undefined { + return this.#app.getSystemIcon(key); + } + + listIconKeys(): string[] { + return Object.keys(this.#app.getSystemIcons()); + } +} + +class CompatRouteResolutionApi implements RouteResolutionApi { + readonly #routeResolver: RouteResolver; + + constructor(routeResolver: RouteResolver) { + this.#routeResolver = routeResolver; + } + + resolve( + anyRouteRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, + options?: RouteResolutionApiResolveOptions | undefined, + ): RouteFunc | undefined { + const legacyRef = convertLegacyRouteRef(anyRouteRef as RouteRef); + return this.#routeResolver.resolve(legacyRef, options?.sourcePath ?? '/'); + } +} + +class ForwardsCompatApis implements ApiHolder { + readonly #componentsApi: ComponentsApi; + readonly #iconsApi: IconsApi; + readonly #routeResolutionApi: RouteResolutionApi; + + constructor(app: AppContext, routeResolver: RouteResolver) { + this.#componentsApi = new CompatComponentsApi(app); + this.#iconsApi = new CompatIconsApi(app); + this.#routeResolutionApi = new CompatRouteResolutionApi(routeResolver); + } + + get(ref: ApiRef): T | undefined { + if (ref.id === componentsApiRef.id) { + return this.#componentsApi as T; + } else if (ref.id === iconsApiRef.id) { + return this.#iconsApi as T; + } else if (ref.id === routeResolutionApiRef.id) { + return this.#routeResolutionApi as T; + } + return undefined; + } +} + +function NewAppApisProvider(props: { children: ReactNode }) { + const app = useApp(); + const versionedRouteResolverContext = useVersionedContext<{ + 1: RouteResolver; + }>('routing-context'); + if (!versionedRouteResolverContext) { + throw new Error('Routing context is not available'); + } + const routeResolver = versionedRouteResolverContext.atVersion(1); + if (!routeResolver) { + throw new Error('RoutingContext v1 not available'); + } + + const appFallbackApis = useMemo( + () => new ForwardsCompatApis(app, routeResolver), + [app, routeResolver], + ); + + return {props.children}; +} export function ForwardsCompatProvider(props: { children: ReactNode }) { - // TODO(Rugvip): Implement - return <>{props.children}; + return {props.children}; } diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index 74b9f209eb..04e2361087 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -16,21 +16,28 @@ import React from 'react'; import { + componentsApiRef, + coreComponentRefs, coreExtensionData, createExtension, + iconsApiRef, + useRouteRef as useNewRouteRef, + createRouteRef as createNewRouteRef, + useApi, } from '@backstage/frontend-plugin-api'; import { createExtensionTester, - renderInTestApp, + renderInTestApp as renderInNewTestApp, } from '@backstage/frontend-test-utils'; import { screen } from '@testing-library/react'; import { compatWrapper } from './compatWrapper'; import { - createRouteRef, useApp, - useRouteRef, + useRouteRef as useOldRouteRef, + createRouteRef as createOldRouteRef, } from '@backstage/core-plugin-api'; import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; +import { renderInTestApp as renderInOldTestApp } from '@backstage/test-utils'; describe('BackwardsCompatProvider', () => { it('should convert the app context', () => { @@ -74,14 +81,58 @@ describe('BackwardsCompatProvider', () => { }); it('should convert the routing context', () => { - const routeRef = createRouteRef({ id: 'test' }); + const routeRef = createOldRouteRef({ id: 'test' }); function Component() { - const link = useRouteRef(routeRef); + const link = useOldRouteRef(routeRef); return
link: {link()}
; } - renderInTestApp(compatWrapper(), { + renderInNewTestApp(compatWrapper(), { + mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, + }); + + expect(screen.getByText('link: /test')).toBeInTheDocument(); + }); +}); + +describe('ForwardsCompatProvider', () => { + it('should convert the app context', async () => { + function Component() { + const components = useApi(componentsApiRef); + const icons = useApi(iconsApiRef); + return ( +
+ components:{' '} + {Object.entries(coreComponentRefs) + .map( + ([name, ref]) => + `${name}=${Boolean(components.getComponent(ref))}`, + ) + .join(', ')} + {'\n'} + icons: {icons.listIconKeys().join(', ')} +
+ ); + } + + await renderInOldTestApp(compatWrapper()); + + expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(` + "components: progress=true, notFoundErrorPage=true, errorBoundaryFallback=true + icons: kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, user, warning" + `); + }); + + it('should convert the routing context', async () => { + const routeRef = createNewRouteRef(); + + function Component() { + const link = useNewRouteRef(routeRef); + return
link: {link?.()}
; + } + + await renderInOldTestApp(compatWrapper(), { mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, }); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts new file mode 100644 index 0000000000..f09060212c --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -0,0 +1,203 @@ +/* + * 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 { + RouteRef as OldRouteRef, + SubRouteRef as OldSubRouteRef, + ExternalRouteRef as OldExternalRouteRef, + createRouteRef as createOldRouteRef, + createSubRouteRef as createOldSubRouteRef, + createExternalRouteRef as createOldExternalRouteRef, +} from '@backstage/core-plugin-api'; +import { + RouteRef as NewRouteRef, + SubRouteRef as NewSubRouteRef, + ExternalRouteRef as NewExternalRouteRef, + createRouteRef as createNewRouteRef, + createSubRouteRef as createNewSubRouteRef, + createExternalRouteRef as createNewExternalRouteRef, +} from '@backstage/frontend-plugin-api'; +import { convertLegacyRouteRef } from './convertLegacyRouteRef'; + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalRouteRef as toInternalNewRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalSubRouteRef as toInternalNewSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalExternalRouteRef as toInternalNewExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef'; + +describe('convertLegacyRouteRef', () => { + it('converts old to new', () => { + const ref1 = createOldRouteRef({ id: 'ref1' }); + const ref2 = createOldRouteRef({ id: 'ref2', params: ['p1', 'p2'] }); + const ref1sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createOldSubRouteRef({ + id: 'sub2', + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createOldExternalRouteRef({ + id: 'ref3', + }); + const ref4 = createOldExternalRouteRef({ + id: 'ref4', + optional: true, + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: NewRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: NewRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: NewExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: NewExternalRouteRef = convertLegacyRouteRef(ref4); + + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + + const ref1Internal = toInternalNewRouteRef(ref1Converted); + const ref2Internal = toInternalNewRouteRef(ref2Converted); + const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); + const ref1sub2Internal = toInternalNewSubRouteRef(ref1sub2Converted); + const ref2sub1Internal = toInternalNewSubRouteRef(ref2sub1Converted); + const ref3Internal = toInternalNewExternalRouteRef(ref3Converted); + const ref4Internal = toInternalNewExternalRouteRef(ref4Converted); + + expect(ref1Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref1}', + ); + expect(ref1Internal.getParams()).toEqual([]); + expect(ref2Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref2}', + ); + expect(ref2Internal.getParams()).toEqual(['p1', 'p2']); + + expect(ref1sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref1sub1Internal.getParams()).toEqual([]); + expect(ref1sub1Internal.getParent()).toBe(ref1); + expect(ref1sub2Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub2}', + ); + expect(ref1sub2Internal.getParams()).toEqual(['p3']); + expect(ref1sub2Internal.getParent()).toBe(ref1); + expect(ref2sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref2sub1Internal.getParams()).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Internal.getParent()).toBe(ref2); + + expect(ref3Internal.getDefaultTarget()).toBe(undefined); + expect(ref3Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref3}', + ); + expect(ref3Internal.getParams()).toEqual([]); + expect(ref4Internal.getDefaultTarget()).toBe('ref2'); + expect(ref4Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref4}', + ); + expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); + }); + + it('converts new to old', () => { + const ref1 = createNewRouteRef(); + const ref2 = createNewRouteRef({ params: ['p1', 'p2'] }); + const ref1sub1 = createNewSubRouteRef({ + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createNewSubRouteRef({ + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createNewSubRouteRef({ + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createNewExternalRouteRef(); + const ref4 = createNewExternalRouteRef({ + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: OldRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: OldRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: OldExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: OldExternalRouteRef = convertLegacyRouteRef(ref4); + + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + + expect(String(ref1Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref1Converted.params).toEqual([]); + expect(String(ref2Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref2Converted.params).toEqual(['p1', 'p2']); + + expect(String(ref1sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1 with parent created at '.*'\}$/, + ); + expect(ref1sub1Converted.params).toEqual([]); + expect(ref1sub1Converted.parent).toBe(ref1); + expect(String(ref1sub2Converted)).toMatch( + /^SubRouteRef\{at \/sub2\/:p3 with parent created at '.*'\}$/, + ); + expect(ref1sub2Converted.params).toEqual(['p3']); + expect(ref1sub2Converted.parent).toBe(ref1); + expect(String(ref2sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1\/:p3 with parent created at '.*'\}$/, + ); + expect(ref2sub1Converted.params).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Converted.parent).toBe(ref2); + + expect(String(ref3Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref3Converted.params).toEqual([]); + expect(ref3Converted.optional).toBe(true); + expect(String(ref4Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref4Converted.params).toEqual(['p1', 'p2']); + expect(ref4Converted.optional).toBe(true); + }); +}); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index c967f9e9b5..40a95a81ff 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -51,8 +51,8 @@ export type ToNewRouteRef< ? RouteRef : T extends LegacySubRouteRef ? SubRouteRef - : T extends LegacyExternalRouteRef - ? ExternalRouteRef + : T extends LegacyExternalRouteRef + ? ExternalRouteRef : never; /** @@ -109,23 +109,115 @@ export function convertLegacyRouteRef( * * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. */ -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: LegacyExternalRouteRef, -): ExternalRouteRef; +export function convertLegacyRouteRef( + ref: LegacyExternalRouteRef, +): ExternalRouteRef; +/** + * A temporary helper to convert a new route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: RouteRef, +): LegacyRouteRef; + +/** + * A temporary helper to convert a new sub route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createSubRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: SubRouteRef, +): LegacySubRouteRef; + +/** + * A temporary helper to convert a new external route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: ExternalRouteRef, +): LegacyExternalRouteRef; export function convertLegacyRouteRef( - ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, -): RouteRef | SubRouteRef | ExternalRouteRef { + ref: + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): + | RouteRef + | SubRouteRef + | ExternalRouteRef + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef { + const isNew = '$$type' in ref; + const oldType = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + // Ref has already been converted - if ('$$type' in ref) { - return ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef; + if (isNew && oldType) { + return ref as any; } - const type = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + if (isNew) { + return convertNewToOld( + ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef, + ); + } + return convertOldToNew(ref, oldType); +} + +function convertNewToOld( + ref: RouteRef | SubRouteRef | ExternalRouteRef, +): LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef { + if (ref.$$type === '@backstage/RouteRef') { + const newRef = toInternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'absolute', + params: newRef.getParams(), + title: newRef.getDescription(), + } as Omit) as unknown as LegacyRouteRef; + } + if (ref.$$type === '@backstage/SubRouteRef') { + const newRef = toInternalSubRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'sub', + parent: convertLegacyRouteRef(newRef.getParent()), + params: newRef.getParams(), + } as Omit) as unknown as LegacySubRouteRef; + } + if (ref.$$type === '@backstage/ExternalRouteRef') { + const newRef = toInternalExternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'external', + optional: true, + params: newRef.getParams(), + defaultTarget: newRef.getDefaultTarget(), + } as Omit) as unknown as LegacyExternalRouteRef; + } + + throw new Error( + `Failed to convert route ref, unknown type '${(ref as any).$$type}'`, + ); +} + +function convertOldToNew( + ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, + type: unknown, +): RouteRef | SubRouteRef | ExternalRouteRef { if (type === 'absolute') { const legacyRef = ref as LegacyRouteRef; const legacyRefStr = String(legacyRef); @@ -185,7 +277,6 @@ export function convertLegacyRouteRef( const newRef = toInternalExternalRouteRef( createExternalRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], - optional: legacyRef.optional, defaultTarget: 'getDefaultTarget' in legacyRef ? (legacyRef.getDefaultTarget as () => string | undefined)() @@ -196,7 +287,6 @@ export function convertLegacyRouteRef( $$type: '@backstage/ExternalRouteRef' as const, version: 'v1', T: newRef.T, - optional: newRef.optional, getParams() { return newRef.getParams(); }, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 85587de268..dce729b811 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.5.18-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + ## 0.5.18-next.2 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 9128438e8e..8886ed9a6d 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.18-next.2", + "version": "0.5.18-next.3", "publishConfig": { "access": "public" }, @@ -39,7 +39,7 @@ "fs-extra": "^11.2.0", "handlebars": "^4.7.3", "inquirer": "^8.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "ora": "^5.3.0", "recursive-readdir": "^2.2.2" }, diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index f784d5ba88..ca52ec530c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -106,3 +106,8 @@ catalog: kubernetes: # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options + +# see https://backstage.io/docs/permissions/getting-started for more on the permission framework +permission: + # setting this to `false` will disable permissions + enabled: true diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index cf2caae72b..f57d4c5f87 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -32,6 +32,7 @@ backend.add(import('@backstage/plugin-catalog-backend-module-logs')); // permission plugin backend.add(import('@backstage/plugin-permission-backend/alpha')); +// See https://backstage.io/docs/permissions/getting-started for how to create your own permission policy backend.add( import('@backstage/plugin-permission-backend-module-allow-all-policy'), ); diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 30aeba5a08..5ea08c7762 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/dev-utils +## 1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + ## 1.0.37-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index f3f835b015..b2b6eb3292 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.37-next.1", + "version": "1.0.37-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 488f48fd4b..4609771fb5 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,14 @@ # e2e-test +## 0.2.19-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.2.19-next.2 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 190d66c7b3..481b011d01 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.19-next.2", + "version": "0.2.19-next.3", "private": true, "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 8593bb38e5..dd419c284b 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/frontend-app-api +## 0.7.5-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.7.5-next.1 ### Patch Changes diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 0375a67482..27d486b6a8 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -44,7 +44,7 @@ export type CreateAppRouteBinder = < externalRoutes: TExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, - KeysWithType> + KeysWithType> >, ) => void; diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 11059214c2..0d462e2721 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.7.5-next.1", + "version": "0.7.5-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/src/extensions/AppNav.tsx b/packages/frontend-app-api/src/extensions/AppNav.tsx index 761c6d5a24..ffef1c7c61 100644 --- a/packages/frontend-app-api/src/extensions/AppNav.tsx +++ b/packages/frontend-app-api/src/extensions/AppNav.tsx @@ -73,9 +73,12 @@ const SidebarNavItem = ( props: (typeof createNavItemExtension.targetDataRef)['T'], ) => { const { icon: Icon, title, routeRef } = props; - const to = useRouteRef(routeRef)(); + const link = useRouteRef(routeRef); + if (!link) { + return null; + } // TODO: Support opening modal, for example, the search one - return ; + return ; }; export const AppNav = createExtension({ diff --git a/packages/frontend-app-api/src/routing/RouteResolver.test.ts b/packages/frontend-app-api/src/routing/RouteResolver.test.ts index ba0a66a380..497e7397cb 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.test.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.test.ts @@ -41,9 +41,7 @@ const subRef2 = createSubRouteRef({ parent: ref1, path: '/foo/:a' }); const subRef3 = createSubRouteRef({ parent: ref2, path: '/bar' }); const subRef4 = createSubRouteRef({ parent: ref2, path: '/bar/:a' }); const externalRef1 = createExternalRouteRef(); -const externalRef2 = createExternalRouteRef({ optional: true }); -const externalRef3 = createExternalRouteRef({ params: ['x'] }); -const externalRef4 = createExternalRouteRef({ optional: true, params: ['x'] }); +const externalRef2 = createExternalRouteRef({ params: ['x'] }); function src(sourcePath: string) { return { sourcePath }; @@ -62,9 +60,7 @@ describe('RouteResolver', () => { undefined, ); expect(r.resolve(externalRef1, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe(undefined); + expect(r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toBe(undefined); }); it('should resolve an absolute route', () => { @@ -87,9 +83,7 @@ describe('RouteResolver', () => { undefined, ); expect(r.resolve(externalRef1, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe(undefined); + expect(r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toBe(undefined); }); it('should resolve an absolute route with a param and with a parent', () => { @@ -112,8 +106,7 @@ describe('RouteResolver', () => { ], new Map([ [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], + [externalRef2, subRef3], ]), '', ); @@ -133,11 +126,7 @@ describe('RouteResolver', () => { '/my-route/my-parent/4x/bar/4a', ); expect(r.resolve(externalRef1, src('/'))?.()).toBe('/my-route'); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe( - '/my-route/my-parent/5x', - ); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe( + expect(r.resolve(externalRef2, src('/'))?.({ x: '6x' })).toBe( '/my-route/my-parent/6x/bar', ); }); @@ -230,8 +219,7 @@ describe('RouteResolver', () => { ], new Map([ [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], + [externalRef2, subRef3], ]), '', ); @@ -282,17 +270,10 @@ describe('RouteResolver', () => { expect(() => r.resolve(externalRef1, src('/'))?.()).toThrow( /^Cannot route.*with parent.*as it has parameters$/, ); - expect(r.resolve(externalRef2, src(l))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src(l))?.({ x: '5x' })).toBe( - '/my-grandparent/my-y/my-parent/5x', + expect(r.resolve(externalRef2, src(l))?.({ x: '5x' })).toBe( + '/my-grandparent/my-y/my-parent/5x/bar', ); - expect(() => r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(externalRef4, src(l))?.({ x: '6x' })).toBe( - '/my-grandparent/my-y/my-parent/6x/bar', - ); - expect(() => r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toThrow( + expect(() => r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toThrow( /^Cannot route.*with parent.*as it has parameters$/, ); }); diff --git a/packages/frontend-app-api/src/routing/RouteResolver.ts b/packages/frontend-app-api/src/routing/RouteResolver.ts index 1ec1822fe9..ff7966c4db 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.ts @@ -195,7 +195,7 @@ export class RouteResolver implements RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined { // First figure out what our target absolute ref is, as well as our target path. diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts index 81d9eddef3..a8f641daff 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -53,8 +53,7 @@ type TargetRouteMap< ExternalRoutes extends { [name: string]: ExternalRouteRef }, > = { [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any + infer Params > ? RouteRef | SubRouteRef : never; @@ -72,7 +71,7 @@ export type CreateAppRouteBinder = < externalRoutes: TExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, - KeysWithType> + KeysWithType> >, ) => void; @@ -95,11 +94,6 @@ export function resolveRouteBindings( if (!externalRoute) { throw new Error(`Key ${key} is not an existing external route`); } - if (!value && !externalRoute.optional) { - throw new Error( - `External route ${key} is required but was undefined`, - ); - } if (value) { result.set(externalRoute, value); } diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index b4090185a3..aded5d7826 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -68,7 +68,7 @@ function makeNode( function makeInstanceWithId( extension: Extension, - config?: TConfig, + config?: TConfigInput, ): AppNode { const node = makeNode(extension, { config }); return { @@ -640,12 +640,12 @@ describe('instantiateAppNodeTree', () => { name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, output: [testDataRef, otherDataRef.optional()], - configSchema: createSchemaFromZod(z => - z.object({ - output: z.string().default('test'), - other: z.number().optional(), - }), - ), + config: { + schema: { + output: z => z.string().default('test'), + other: z => z.number().optional(), + }, + }, factory({ config }) { return [ testDataRef(config.output), @@ -914,7 +914,7 @@ describe('instantiateAppNodeTree', () => { namespace: 'app', name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: [], + output: [testDataRef], factory() { const error = new Error('NOPE'); error.name = 'NopeError'; @@ -981,11 +981,12 @@ describe('instantiateAppNodeTree', () => { createAppNodeInstance({ node: makeNode( resolveExtensionDefinition( + // @ts-expect-error createExtension({ namespace: 'app', name: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: [], + output: [], // Output not declared factory({}) { return [testDataRef('test')] as any; }, diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 8af2aef070..65f3f77640 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -48,6 +48,7 @@ function makeExtDef( name, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + override: () => ({} as ExtensionDefinition), } as ExtensionDefinition; } diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 40961b558f..302d541b18 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/frontend-plugin-api +## 0.7.0-next.2 + +### Minor Changes + +- 72754db: **BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + + This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. + +### Patch Changes + +- 210d066: Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.6.8-next.1 ### Patch Changes diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index d37f0a3a56..f39add924a 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -189,6 +189,27 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef; }; +// @public +export const ApiBlueprint: ExtensionBlueprint< + 'api', + undefined, + undefined, + { + factory: AnyApiFactory; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + factory: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + } +>; + export { ApiFactory }; export { ApiHolder }; @@ -240,6 +261,50 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } +// @public +export const AppRootElementBlueprint: ExtensionBlueprint< + 'app-root-element', + undefined, + undefined, + { + element: JSX.Element | (() => JSX.Element); + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + never +>; + +// @public +export const AppRootWrapperBlueprint: ExtensionBlueprint< + 'app-root-wrapper', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >; + } +>; + export { AppTheme }; export { AppThemeApi }; @@ -275,17 +340,22 @@ export { BackstageIdentityResponse }; // @public (undocumented) export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { + [id in string]: ExtensionDefinition; + } = {}, > { // (undocumented) readonly $$type: '@backstage/BackstagePlugin'; // (undocumented) - readonly externalRoutes: ExternalRoutes; + readonly externalRoutes: TExternalRoutes; + // (undocumented) + getExtension(id: TId): TExtensionMap[TId]; // (undocumented) readonly id: string; // (undocumented) - readonly routes: Routes; + readonly routes: TRoutes; } export { BackstageUserIdentity }; @@ -376,7 +446,7 @@ export type CoreNotFoundErrorPageProps = { // @public (undocumented) export type CoreProgressProps = {}; -// @public (undocumented) +// @public @deprecated (undocumented) export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -396,7 +466,15 @@ export function createApiExtension< configSchema?: PortableSchema; inputs?: TInputs; }, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createApiExtension { @@ -412,7 +490,7 @@ export { createApiFactory }; export { createApiRef }; -// @public +// @public @deprecated export function createAppRootElementExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -434,7 +512,7 @@ export function createAppRootElementExtension< }) => JSX_2.Element); }): ExtensionDefinition; -// @public +// @public @deprecated export function createAppRootWrapperExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -492,7 +570,15 @@ export function createComponentExtension< inputs: Expand>; }) => ComponentType; }; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createComponentExtension { @@ -524,36 +610,37 @@ export function createExtension< } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, >( options: CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput >, ): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName >; // @public @deprecated (undocumented) @@ -562,33 +649,14 @@ export function createExtension< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, >( options: LegacyCreateExtensionOptions< TOutput, TInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, -): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) ->; +): ExtensionDefinition; // @public export function createExtensionBlueprint< @@ -603,16 +671,21 @@ export function createExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, TDataRefs extends { [name in string]: AnyExtensionDataRef; } = never, >( options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -621,10 +694,12 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, - TInputs, - UExtraOutput, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { @@ -642,6 +717,9 @@ export function createExtensionBlueprint< // @public (undocumented) export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -661,8 +739,8 @@ export type CreateExtensionBlueprintOptions< [name in string]: AnyExtensionDataRef; }, > = { - kind: string; - namespace?: string; + kind: TKind; + namespace?: TNamespace; attachTo: { id: string; input: string; @@ -670,6 +748,7 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; + name?: TName; config?: { schema: TConfigSchema; }; @@ -742,6 +821,9 @@ export function createExtensionInput< // @public (undocumented) export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -752,16 +834,14 @@ export type CreateExtensionOptions< } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, > = { - kind?: string; - namespace?: string; - name?: string; + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string; @@ -769,20 +849,14 @@ export type CreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: Array; - configSchema?: PortableSchema; config?: { schema: TConfigSchema; }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: { + [key in keyof TConfigSchema]: z.infer>; + }; inputs: Expand>; }): Iterable; } & VerifyExtensionFactoryOutput; @@ -799,13 +873,11 @@ export function createExternalRouteRef< [param in TParamKeys]: string; } | undefined = undefined, - TOptional extends boolean = false, TParamKeys extends string = string, >(options?: { readonly params?: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[]; - optional?: TOptional; defaultTarget?: string; }): ExternalRouteRef< keyof TParams extends never @@ -814,11 +886,10 @@ export function createExternalRouteRef< ? TParams : { [param in TParamKeys]: string; - }, - TOptional + } >; -// @public +// @public @deprecated export function createNavItemExtension(options: { namespace?: string; name?: string; @@ -831,7 +902,12 @@ export function createNavItemExtension(options: { }, { title?: string | undefined; - } + }, + never, + never, + string | undefined, + string | undefined, + string | undefined >; // @public (undocumented) @@ -848,13 +924,21 @@ export namespace createNavItemExtension { >; } -// @public +// @public @deprecated export function createNavLogoExtension(options: { name?: string; namespace?: string; logoIcon: JSX.Element; logoFull: JSX.Element; -}): ExtensionDefinition<{}, {}>; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createNavLogoExtension { @@ -869,7 +953,7 @@ export namespace createNavLogoExtension { >; } -// @public +// @public @deprecated export function createPageExtension< TConfig extends { path: string; @@ -902,11 +986,22 @@ export function createPageExtension< // @public (undocumented) export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin; + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +>; // @public export function createRouteRef< @@ -928,7 +1023,7 @@ export function createRouteRef< } >; -// @public +// @public @deprecated export function createRouterExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -967,7 +1062,7 @@ export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, ): PortableSchema; -// @public (undocumented) +// @public @deprecated (undocumented) export function createSignInPageExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -1006,12 +1101,20 @@ export function createSubRouteRef< parent: RouteRef; }): MakeSubRouteRef, ParentParams>; -// @public (undocumented) +// @public @deprecated (undocumented) export function createThemeExtension( theme: AppTheme, -): ExtensionDefinition<{}, {}>; +): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createThemeExtension { const // (undocumented) themeDataRef: ConfigurableExtensionDataRef< @@ -1021,13 +1124,21 @@ export namespace createThemeExtension { >; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; -}): ExtensionDefinition<{}, {}>; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createTranslationExtension { const // (undocumented) translationDataRef: ConfigurableExtensionDataRef< @@ -1081,6 +1192,9 @@ export interface Extension { // @public (undocumented) export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -1092,7 +1206,6 @@ export interface ExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown; }, @@ -1105,57 +1218,87 @@ export interface ExtensionBlueprint< > { // (undocumented) dataRefs: TDataRefs; + // (undocumented) make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + makeWithOverrides< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, - >( - args: { - namespace?: string; - name?: string; - attachTo?: { - id: string; - input: string; - }; - disabled?: boolean; - inputs?: TInputs; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; - }; - }; - } & ( - | ({ - factory( - originalFactory: ( - params: TParams, - context?: { - config?: TConfig; - inputs?: Expand>; - }, - ) => Iterable>, - context: { - node: AppNode; - config: TConfig & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & VerifyExtensionFactoryOutput< - UOutput & UExtraOutput, - UFactoryOutput - >) - | { - params: TParams; + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; } - ), - ): ExtensionDefinition< + >; + }, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -1168,7 +1311,12 @@ export interface ExtensionBlueprint< >; }> > & - TConfigInput + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName >; } @@ -1188,7 +1336,17 @@ export interface ExtensionBoundaryProps { // @public (undocumented) export type ExtensionDataContainer = - { + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { get( ref: ExtensionDataRef, ): UExtensionData extends ExtensionDataRef @@ -1241,7 +1399,23 @@ export type ExtensionDataValues = { }; // @public (undocumented) -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> { // (undocumented) $$type: '@backstage/ExtensionDefinition'; // (undocumented) @@ -1254,11 +1428,84 @@ export interface ExtensionDefinition { // (undocumented) readonly disabled: boolean; // (undocumented) - readonly kind?: string; + readonly kind?: TKind; // (undocumented) - readonly name?: string; + readonly name?: TName; // (undocumented) - readonly namespace?: string; + readonly namespace?: TNamespace; + // (undocumented) + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + >( + args: { + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >, + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + TNamespace, + TName + >; } // @public (undocumented) @@ -1300,13 +1547,10 @@ export interface ExtensionOverridesOptions { // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, > { // (undocumented) readonly $$type: '@backstage/ExternalRouteRef'; // (undocumented) - readonly optional: TOptional; - // (undocumented) readonly T: TParams; } @@ -1340,6 +1584,9 @@ export { googleAuthApiRef }; // @public (undocumented) export const IconBundleBlueprint: ExtensionBlueprint< + 'icon-bundle', + 'app', + undefined, { icons: { [x: string]: IconComponent; @@ -1352,16 +1599,7 @@ export const IconBundleBlueprint: ExtensionBlueprint< 'core.icons', {} >, - { - [x: string]: ExtensionInput< - AnyExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; - }, - AnyExtensionDataRef, + {}, { icons: string; test: string; @@ -1412,9 +1650,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, > { // (undocumented) attachTo: { @@ -1422,24 +1657,13 @@ export interface LegacyCreateExtensionOptions< input: string; }; // (undocumented) - config?: { - schema: TConfigSchema; - }; - // @deprecated (undocumented) configSchema?: PortableSchema; // (undocumented) disabled?: boolean; // (undocumented) factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: TConfig; inputs: Expand>; }): Expand>; // (undocumented) @@ -1472,6 +1696,73 @@ export interface LegacyExtensionInput< export { microsoftAuthApiRef }; +// @public +export const NavItemBlueprint: ExtensionBlueprint< + 'nav-item', + undefined, + undefined, + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + {}, + {}, + { + target: ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + } +>; + +// @public +export const NavLogoBlueprint: ExtensionBlueprint< + 'nav-logo', + undefined, + undefined, + { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }, + ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >, + {}, + {}, + {}, + { + logoElements: ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >; + } +>; + export { OAuthApi }; export { OAuthRequestApi }; @@ -1490,23 +1781,54 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +// @public +export const PageBlueprint: ExtensionBlueprint< + 'page', + undefined, + undefined, + { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + RouteRef & { + optional: true; + } + >, + {}, + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + never +>; + export { PendingOAuthRequest }; // @public (undocumented) export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { // (undocumented) - extensions?: ExtensionDefinition[]; + extensions?: TExtensions; // (undocumented) - externalRoutes?: ExternalRoutes; + externalRoutes?: TExternalRoutes; // (undocumented) featureFlags?: FeatureFlagConfig[]; // (undocumented) - id: string; + id: TId; // (undocumented) - routes?: Routes; + routes?: TRoutes; } // @public (undocumented) @@ -1553,6 +1875,35 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; +// @public (undocumented) +export const RouterBlueprint: ExtensionBlueprint< + 'app-router-component', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >; + } +>; + // @public export interface RouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -1570,7 +1921,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } @@ -1587,6 +1938,31 @@ export { SessionApi }; export { SessionState }; +// @public +export const SignInPageBlueprint: ExtensionBlueprint< + 'sign-in-page', + undefined, + undefined, + { + loader: () => Promise>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >; + } +>; + export { StorageApi }; export { storageApiRef }; @@ -1605,6 +1981,62 @@ export interface SubRouteRef< readonly T: TParams; } +// @public +export const ThemeBlueprint: ExtensionBlueprint< + 'theme', + 'app', + undefined, + { + theme: AppTheme; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + theme: ConfigurableExtensionDataRef; + } +>; + +// @public +export const TranslationBlueprint: ExtensionBlueprint< + 'translation', + undefined, + undefined, + { + resource: TranslationResource | TranslationMessages; + }, + ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >, + {}, + {}, + {}, + { + translation: ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >; + } +>; + export { TranslationMessages }; export { TranslationMessagesOptions }; @@ -1631,18 +2063,13 @@ export function useComponentRef( ref: ComponentRef, ): ComponentType; -// @public -export function useRouteRef< - TOptional extends boolean, - TParams extends AnyRouteRefParams, ->( - routeRef: ExternalRouteRef, -): TOptional extends true ? RouteFunc | undefined : RouteFunc; - // @public export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; + routeRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): RouteFunc | undefined; // @public export function useRouteRefParams( diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 5ee4667c3a..8f729a5c4d 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.6.8-next.1", + "version": "0.7.0-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts index 06388cc966..30569bde15 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts @@ -60,7 +60,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts new file mode 100644 index 0000000000..3b865a78bb --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2024 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 { createExtensionInput } from '../wiring'; +import { ApiBlueprint } from './ApiBlueprint'; +import { createApiFactory, createApiRef } from '@backstage/core-plugin-api'; + +describe('ApiBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = createApiFactory({ + api, + deps: {}, + factory: () => ({ foo: 'bar' }), + }); + + const extension = ApiBlueprint.make({ + params: { + factory, + }, + namespace: 'test', + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should create an extension with custom factory', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = jest.fn(() => ({ foo: 'bar' })); + + const extension = ApiBlueprint.makeWithOverrides({ + config: { + schema: { + test: z => z.string().default('test'), + }, + }, + inputs: { + test: createExtensionInput([ApiBlueprint.dataRefs.factory]), + }, + namespace: api.id, + factory(originalFactory, { config: _config, inputs: _inputs }) { + return originalFactory({ + factory: createApiFactory({ + api, + deps: {}, + factory, + }), + }); + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "test": { + "default": "test", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": { + "test": { + "$$type": "@backstage/ExtensionInput", + "config": { + "optional": false, + "singleton": false, + }, + "extensionData": [ + [Function], + ], + }, + }, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts new file mode 100644 index 0000000000..b5ba9e05f6 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createApiExtension } from '../extensions/createApiExtension'; +import { AnyApiFactory } from '@backstage/core-plugin-api'; + +/** + * Creates utility API extensions. + * + * @public + */ +export const ApiBlueprint = createExtensionBlueprint({ + kind: 'api', + attachTo: { id: 'app', input: 'apis' }, + output: [createApiExtension.factoryDataRef], + dataRefs: { + factory: createApiExtension.factoryDataRef, + }, + *factory(params: { factory: AnyApiFactory }) { + yield createApiExtension.factoryDataRef(params.factory); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx new file mode 100644 index 0000000000..3fb8d7b37a --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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 { AppRootElementBlueprint } from './AppRootElementBlueprint'; + +describe('AppRootElementBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = AppRootElementBlueprint.make({ + params: { + element:
, + }, + }); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "elements", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-element", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts new file mode 100644 index 0000000000..bc6304923a --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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 { coreExtensionData, createExtensionBlueprint } from '../wiring'; + +/** + * Creates extensions that render a React element at the app root, outside of + * the app layout. This is useful for example for shared popups and similar. + * + * @public + */ +export const AppRootElementBlueprint = createExtensionBlueprint({ + kind: 'app-root-element', + attachTo: { id: 'app/root', input: 'elements' }, + output: [coreExtensionData.reactElement], + *factory(params: { element: JSX.Element | (() => JSX.Element) }) { + yield coreExtensionData.reactElement( + typeof params.element === 'function' ? params.element() : params.element, + ); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx new file mode 100644 index 0000000000..f7feab1d46 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright 2024 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 { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { PageBlueprint } from './PageBlueprint'; +import { waitFor } from '@testing-library/react'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '../wiring'; + +describe('AppRootWrapperBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
Hello
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "wrappers", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-wrapper", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should render the simple component wrapper', async () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
Hello
, + }, + }); + + const { getByText } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ) + .add(extension) + .render(); + + await waitFor(() => expect(getByText('Hello')).toBeInTheDocument()); + }); + + it('should render the complex component wrapper', async () => { + const extension = AppRootWrapperBlueprint.makeWithOverrides({ + namespace: 'ns', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( +
+ {children} + {inputs.children.flatMap(c => + c.get(coreExtensionData.reactElement), + )} +
+ ), + }); + }, + }); + + const { getByText, getByTestId } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
Hi
, + }, + }), + ) + .add(extension, { config: { name: 'Robin' } }) + .add( + createExtension({ + attachTo: { id: 'app-root-wrapper:ns/test', input: 'children' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
Its Me
)], + }), + ) + .render(); + + await waitFor(() => { + expect(getByText('Hi')).toBeInTheDocument(); + expect(getByTestId('Robin-1')).toBeInTheDocument(); + expect(getByText('Its Me')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx new file mode 100644 index 0000000000..c54686f54f --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createAppRootWrapperExtension } from '../extensions/createAppRootWrapperExtension'; + +/** + * Creates a extensions that render a React wrapper at the app root, enclosing + * the app layout. This is useful for example for adding global React contexts + * and similar. + * + * @public + */ +export const AppRootWrapperBlueprint = createExtensionBlueprint({ + kind: 'app-root-wrapper', + attachTo: { id: 'app/root', input: 'wrappers' }, + output: [createAppRootWrapperExtension.componentDataRef], + dataRefs: { + component: createAppRootWrapperExtension.componentDataRef, + }, + *factory(params: { Component: ComponentType> }) { + // todo(blam): not sure that this wrapping is even necessary anymore. + const Component = (props: PropsWithChildren<{}>) => { + return {props.children}; + }; + + yield createAppRootWrapperExtension.componentDataRef(Component); + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts similarity index 100% rename from packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx new file mode 100644 index 0000000000..94b9e7b8e6 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createRouteRef } from '../routing'; +import { NavItemBlueprint } from './NavItemBlueprint'; + +describe('NavItemBlueprint', () => { + const mockRouteRef = createRouteRef(); + const MockIcon = () => null; + + it('should return an extension with sensible defaults', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-item", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the correct extension data', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'TEST', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); + + it('should allow overriding of the title using config', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension, { + config: { title: 'OVERRIDDEN' }, + }); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'OVERRIDDEN', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts new file mode 100644 index 0000000000..20f57421c7 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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 { IconComponent } from '@backstage/core-plugin-api'; +import { RouteRef } from '../routing'; +import { createExtensionBlueprint } from '../wiring'; +import { createNavItemExtension } from '../extensions/createNavItemExtension'; + +/** + * Creates extensions that make up the items of the nav bar. + * + * @public + */ +export const NavItemBlueprint = createExtensionBlueprint({ + kind: 'nav-item', + attachTo: { id: 'app/nav', input: 'items' }, + output: [createNavItemExtension.targetDataRef], + dataRefs: { + target: createNavItemExtension.targetDataRef, + }, + factory: ( + { + icon, + routeRef, + title, + }: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + { config }, + ) => [ + createNavItemExtension.targetDataRef({ + title: config.title ?? title, + icon, + routeRef, + }), + ], + config: { + schema: { + title: z => z.string().optional(), + }, + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx new file mode 100644 index 0000000000..4a96475cf5 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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 { NavLogoBlueprint } from './NavLogoBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('NavLogoBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = NavLogoBlueprint.make({ + params: { + logoFull:
Logo Full
, + logoIcon:
Logo Icon
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "logos", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-logo", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return a valid component ref', () => { + const logoFull =
Logo Full
; + const logoIcon =
Logo Icon
; + + const extension = NavLogoBlueprint.make({ + name: 'test', + params: { + logoFull, + logoIcon, + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavLogoBlueprint.dataRefs.logoElements)).toEqual({ + logoFull, + logoIcon, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts new file mode 100644 index 0000000000..8066646ca3 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createNavLogoExtension } from '../extensions/createNavLogoExtension'; + +/** + * Creates an extension that replaces the logo in the nav bar with your own. + * + * @public + */ +export const NavLogoBlueprint = createExtensionBlueprint({ + kind: 'nav-logo', + attachTo: { id: 'app/nav', input: 'logos' }, + output: [createNavLogoExtension.logoElementsDataRef], + dataRefs: { + logoElements: createNavLogoExtension.logoElementsDataRef, + }, + *factory({ + logoIcon, + logoFull, + }: { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }) { + yield createNavLogoExtension.logoElementsDataRef({ + logoIcon, + logoFull, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx new file mode 100644 index 0000000000..c4bcb4b467 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2024 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 { createRouteRef } from '../routing'; +import { PageBlueprint } from './PageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionInput, +} from '../wiring'; +import { waitFor } from '@testing-library/react'; + +describe('PageBlueprint', () => { + const mockRouteRef = createRouteRef(); + + it('should return an extension when calling make with sensible defaults', () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
Test
), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + expect(myPage).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/routes", + "input": "routes", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "page", + "name": "test-page", + "namespace": undefined, + "output": [ + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a routeRef with the input routeRef', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
Test
), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + const tester = createExtensionTester(myPage); + + // TODO(blam): test for the routePath output doesn't work, due to the the way the test harness works + // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + + const { getByTestId } = tester.render(); + + await waitFor(() => expect(getByTestId('test')).toBeInTheDocument()); + }); + + it('should allow defining additional inputs to the extension', async () => { + const myPage = PageBlueprint.makeWithOverrides({ + name: 'test-page', + inputs: { + cards: createExtensionInput([coreExtensionData.reactElement], { + optional: false, + singleton: false, + }), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + loader: async () => ( +
+ {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} +
+ ), + defaultPath: '/test', + routeRef: mockRouteRef, + }); + }, + }); + + const CardBlueprint = createExtensionBlueprint({ + kind: 'card', + attachTo: { id: 'page:test-page', input: 'cards' }, + output: [coreExtensionData.reactElement], + factory() { + return [ + coreExtensionData.reactElement( +
I'm a lovely card
, + ), + ]; + }, + }); + + const tester = createExtensionTester(myPage).add( + CardBlueprint.make({ name: 'card', params: {} }), + ); + + const { getByTestId, getByText } = tester.render(); + + await waitFor(() => expect(getByTestId('card')).toBeInTheDocument()); + await waitFor(() => + expect(getByText("I'm a lovely card")).toBeInTheDocument(), + ); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx new file mode 100644 index 0000000000..b224ea491e --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2024 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, { lazy } from 'react'; +import { RouteRef } from '../routing'; +import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +import { ExtensionBoundary } from '../components'; + +/** + * Createx extensions that are routable React page components. + * + * @public + */ +export const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + }, + }, + *factory( + { + defaultPath, + loader, + routeRef, + }: { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef; + }, + { config, node }, + ) { + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.routePath(config.path ?? defaultPath); + yield coreExtensionData.reactElement( + + + , + ); + + if (routeRef) { + yield coreExtensionData.routeRef(routeRef); + } + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx new file mode 100644 index 0000000000..fe40aaa8d7 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright 2024 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 { RouterBlueprint } from './RouterBlueprint'; +import { MemoryRouter } from 'react-router-dom'; +import { render, waitFor } from '@testing-library/react'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createExtensionOverrides, +} from '../wiring'; +import { MockConfigApi } from '@backstage/test-utils'; +import { PageBlueprint } from './PageBlueprint'; + +describe('RouterBlueprint', () => { + it('should return an extension when calling make with sensible defaults', () => { + const extension = RouterBlueprint.make({ + params: { + Component: props =>
{props.children}
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "router", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-router-component", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should work with simple options', async () => { + const extension = RouterBlueprint.make({ + namespace: 'test', + params: { + Component: ({ children }) => ( + +
{children}
+
+ ), + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ], + }), + ], + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router')).toBeInTheDocument(); + }); + }); + + it('should work with complex options and props', async () => { + const extension = RouterBlueprint.makeWithOverrides({ + namespace: 'test', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( + +
+ {children} +
+
+ ), + }); + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createExtension({ + namespace: 'test', + attachTo: { + id: 'app-router-component:test/test', + input: 'children', + }, + output: [coreExtensionData.reactElement], + *factory() { + yield coreExtensionData.reactElement(
); + }, + }), + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ], + }), + ], + config: new MockConfigApi({ + app: { + extensions: [ + { + 'app-router-component:test/test': { config: { name: 'Robin' } }, + }, + ], + }, + }), + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router-Robin-1')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx new file mode 100644 index 0000000000..24ee805659 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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 { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createRouterExtension } from '../extensions/createRouterExtension'; + +/** @public */ +export const RouterBlueprint = createExtensionBlueprint({ + kind: 'app-router-component', + attachTo: { id: 'app/root', input: 'router' }, + output: [createRouterExtension.componentDataRef], + dataRefs: { + component: createRouterExtension.componentDataRef, + }, + *factory({ Component }: { Component: ComponentType> }) { + yield createRouterExtension.componentDataRef(Component); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx new file mode 100644 index 0000000000..f97798a64c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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 { SignInPageBlueprint } from './SignInPageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { screen, waitFor } from '@testing-library/react'; +import { coreExtensionData, createExtension } from '../wiring'; + +describe('SignInPageBlueprint', () => { + it('should create an extension with sensible defaults', () => { + expect( + SignInPageBlueprint.make({ + params: { loader: async () => () =>
}, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "signInPage", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "sign-in-page", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the component as the componentRef', async () => { + const MockSignInPage = () =>
; + + const extension = SignInPageBlueprint.make({ + name: 'test', + params: { loader: async () => () => }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); + + createExtensionTester( + createExtension({ + name: 'dummy', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ element:
}), + }), + ) + .add(extension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx new file mode 100644 index 0000000000..f87ae91452 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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, { ComponentType, lazy } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createSignInPageExtension } from '../extensions/createSignInPageExtension'; +import { SignInPageProps } from '@backstage/core-plugin-api'; +import { ExtensionBoundary } from '../components'; + +/** + * Creates an extension that replaces the sign in page. + * + * @public + */ +export const SignInPageBlueprint = createExtensionBlueprint({ + kind: 'sign-in-page', + attachTo: { id: 'app/root', input: 'signInPage' }, + output: [createSignInPageExtension.componentDataRef], + dataRefs: { + component: createSignInPageExtension.componentDataRef, + }, + *factory( + { + loader, + }: { + loader: () => Promise>; + }, + { node }, + ) { + const ExtensionComponent = lazy(() => + loader().then(component => ({ default: component })), + ); + + yield createSignInPageExtension.componentDataRef(props => ( + + + + )); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts new file mode 100644 index 0000000000..40323b6a31 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 { AppTheme } from '@backstage/core-plugin-api'; +import { ThemeBlueprint } from './ThemeBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('ThemeBlueprint', () => { + const theme = { + id: 'light', + colors: { primary: 'blue' }, + variant: 'dark', + title: 'lols', + Provider: (_: { children: React.ReactNode }) => null, + } as AppTheme; + + it('should create an extension with sensible defaults', () => { + expect(ThemeBlueprint.make({ name: 'light', params: { theme } })) + .toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "themes", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "theme", + "name": "light", + "namespace": "app", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the theme as an themeDataRef', async () => { + const extension = ThemeBlueprint.make({ params: { theme } }); + + expect( + createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), + ).toEqual(theme); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts new file mode 100644 index 0000000000..686c498c06 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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 { AppTheme } from '@backstage/core-plugin-api'; +import { createExtensionBlueprint } from '../wiring'; +import { createThemeExtension } from '../extensions/createThemeExtension'; + +/** + * Creates an extension that adds/replaces an app theme. + * + * @public + */ +export const ThemeBlueprint = createExtensionBlueprint({ + kind: 'theme', + namespace: 'app', + attachTo: { id: 'app', input: 'themes' }, + output: [createThemeExtension.themeDataRef], + dataRefs: { + theme: createThemeExtension.themeDataRef, + }, + factory: ({ theme }: { theme: AppTheme }) => [ + createThemeExtension.themeDataRef(theme), + ], +}); diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts new file mode 100644 index 0000000000..dfb918053a --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + createTranslationMessages, + createTranslationRef, +} from '../translation'; +import { TranslationBlueprint } from './TranslationBlueprint'; + +describe('TranslationBlueprint', () => { + const translationRef = createTranslationRef({ + id: 'translationRefId', + messages: { + test: 'test', + }, + }); + + const messages = createTranslationMessages({ + ref: translationRef, + messages: { + test: 'test2', + }, + }); + + it('should return an extension instance with sane defaults', () => { + expect( + TranslationBlueprint.make({ + name: 'blob', + params: { + resource: messages, + }, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "translations", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "translation", + "name": "blob", + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a translation data ref', () => { + const extension = TranslationBlueprint.make({ + name: 'blob', + params: { + resource: messages, + }, + }); + + expect( + createExtensionTester(extension).data( + TranslationBlueprint.dataRefs.translation, + ), + ).toBe(messages); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts new file mode 100644 index 0000000000..bdb36e2191 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createTranslationExtension } from '../extensions/createTranslationExtension'; +import { TranslationMessages, TranslationResource } from '../translation'; + +/** + * Creates an extension that adds translations to your app. + * + * @public + */ +export const TranslationBlueprint = createExtensionBlueprint({ + kind: 'translation', + attachTo: { id: 'app', input: 'translations' }, + output: [createTranslationExtension.translationDataRef], + dataRefs: { + translation: createTranslationExtension.translationDataRef, + }, + factory: ({ + resource, + }: { + resource: TranslationResource | TranslationMessages; + }) => [createTranslationExtension.translationDataRef(resource)], +}); diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts new file mode 100644 index 0000000000..85f7a99a2c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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 { ApiBlueprint } from './ApiBlueprint'; +export { AppRootElementBlueprint } from './AppRootElementBlueprint'; +export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; +export { NavItemBlueprint } from './NavItemBlueprint'; +export { NavLogoBlueprint } from './NavLogoBlueprint'; +export { PageBlueprint } from './PageBlueprint'; +export { RouterBlueprint } from './RouterBlueprint'; +export { SignInPageBlueprint } from './SignInPageBlueprint'; +export { ThemeBlueprint } from './ThemeBlueprint'; +export { TranslationBlueprint } from './TranslationBlueprint'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index c4d0239094..8f8a0cd9ac 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -48,6 +48,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); @@ -85,6 +86,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 93346f1ec9..b1db2a0106 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -24,7 +24,10 @@ import { import { AnyExtensionInputMap } from '../wiring/createExtension'; import { Expand } from '../types'; -/** @public */ +/** + * @public + * @deprecated Use {@link ApiBlueprint} instead. + */ export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx index c8bf6ee355..6e80f6fe13 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx @@ -41,6 +41,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension).render(); @@ -88,6 +89,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension, { config: { name: 'Robin' } }) diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts index 8be82d5540..c8f7bdc366 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts @@ -30,6 +30,7 @@ import { * the app layout. This is useful for example for shared popups and similar. * * @public + * @deprecated Use {@link AppRootElementBlueprint} instead. */ export function createAppRootElementExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx index d21cfa7583..993c4fdee3 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx @@ -42,6 +42,7 @@ describe('createAppRootWrapperExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester( @@ -94,6 +95,7 @@ describe('createAppRootWrapperExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index ce2eb89f25..4f70f29470 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * and similar. * * @public + * @deprecated Use {@link AppRootWrapperBlueprint} instead. */ export function createAppRootWrapperExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 2a2c47835a..d694440427 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -21,7 +21,9 @@ import { RouteRef } from '../routing'; /** * Helper for creating extensions for a nav item. + * * @public + * @deprecated Use {@link NavItemBlueprint} instead. */ export function createNavItemExtension(options: { namespace?: string; diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx index 8c54743f1d..17187a339d 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx @@ -41,6 +41,7 @@ describe('createNavLogoExtension', () => { logos: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx index 1f2419bd02..02c32e7f34 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx @@ -18,7 +18,9 @@ import { createExtension, createExtensionDataRef } from '../wiring'; /** * Helper for creating extensions for a nav logos. + * * @public + * @deprecated Use {@link NavLogoBlueprint} instead. */ export function createNavLogoExtension(options: { name?: string; diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 374400fce8..e5dd4027c0 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -56,6 +56,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); expect( @@ -78,6 +79,7 @@ describe('createPageExtension', () => { kind: 'page', attachTo: { id: 'other', input: 'place' }, configSchema: expect.anything(), + override: expect.any(Function), disabled: true, inputs: { first: createExtensionInput({ @@ -115,6 +117,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index cd434a3ffc..bef642e465 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -31,6 +31,7 @@ import { ExtensionDefinition } from '../wiring/createExtension'; * Helper for creating extensions for a routable React page component. * * @public + * @deprecated Use {@link PageBlueprint} instead. */ export function createPageExtension< TConfig extends { path: string }, diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx index 7109e1a8aa..2c16076fcf 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx @@ -50,6 +50,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); @@ -119,6 +120,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx index 12f946e8a7..943711f5e0 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * MemoryRouter in tests, or to add additional props to a BrowserRouter. * * @public + * @deprecated Use {@link RouterBlueprint} instead. */ export function createRouterExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx index 312d30c78c..3f41202f11 100644 --- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -30,6 +30,7 @@ import { SignInPageProps } from '@backstage/core-plugin-api'; /** * * @public + * @deprecated Use {@link SignInPageBlueprint} instead. */ export function createSignInPageExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index 1740f05a5a..e523fd9c05 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -17,7 +17,10 @@ import { createExtension, createExtensionDataRef } from '../wiring'; import { AppTheme } from '@backstage/core-plugin-api'; -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export function createThemeExtension(theme: AppTheme) { return createExtension({ kind: 'theme', @@ -31,7 +34,10 @@ export function createThemeExtension(theme: AppTheme) { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export namespace createThemeExtension { export const themeDataRef = createExtensionDataRef().with({ id: 'core.theme.theme', diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts index 975901b8e5..019c565cbd 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts @@ -46,6 +46,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -84,6 +85,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -120,6 +122,7 @@ describe('createTranslationExtension', () => { version: 'v1', kind: 'translation', namespace: 'test', + override: expect.any(Function), name: 'sv', attachTo: { id: 'app', input: 'translations' }, disabled: false, diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts index 9445e32e24..c4d2f6037d 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -17,7 +17,10 @@ import { TranslationMessages, TranslationResource } from '../translation'; import { createExtension, createExtensionDataRef } from '../wiring'; -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; @@ -34,7 +37,10 @@ export function createTranslationExtension(options: { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export namespace createTranslationExtension { export const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 267201db81..562cb728cb 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -25,4 +25,3 @@ export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; export { createComponentExtension } from './createComponentExtension'; export { createTranslationExtension } from './createTranslationExtension'; -export { IconBundleBlueprint } from './IconBundleBlueprint'; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 263380b1f8..957ca50bba 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -22,6 +22,7 @@ export * from './analytics'; export * from './apis'; +export * from './blueprints'; export * from './components'; export * from './extensions'; export * from './icons'; diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts index 727dc1f53e..97d3dd2264 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts @@ -26,7 +26,6 @@ describe('ExternalRouteRef', () => { const routeRef: ExternalRouteRef = createExternalRouteRef(); const internal = toInternalExternalRouteRef(routeRef); expect(internal.getParams()).toEqual([]); - expect(internal.optional).toBe(false); expect(String(internal)).toMatch( /^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/, @@ -35,16 +34,6 @@ describe('ExternalRouteRef', () => { expect(String(internal)).toBe('ExternalRouteRef{some-id}'); }); - it('should be created as optional', () => { - const routeRef: ExternalRouteRef = createExternalRouteRef({ - params: [], - optional: true, - }); - const internal = toInternalExternalRouteRef(routeRef); - expect(internal.getParams()).toEqual([]); - expect(internal.optional).toEqual(true); - }); - it('should be created with params', () => { const routeRef: ExternalRouteRef<{ x: string; @@ -52,63 +41,39 @@ describe('ExternalRouteRef', () => { }> = createExternalRouteRef({ params: ['x', 'y'] }); const internal = toInternalExternalRouteRef(routeRef); expect(internal.getParams()).toEqual(['x', 'y']); - expect(internal.optional).toEqual(false); - }); - - it('should be created as optional with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ params: ['x', 'y'], optional: true }); - const internal = toInternalExternalRouteRef(routeRef); - expect(internal.getParams()).toEqual(['x', 'y']); - expect(internal.optional).toEqual(true); }); it('should properly infer and validate parameter types and assignments', () => { - function checkRouteRef< - T extends AnyRouteRefParams, - TOptional extends boolean, - TCheck extends TOptional, - >( - _ref: ExternalRouteRef, + function checkRouteRef( + _ref: ExternalRouteRef, _params: T extends undefined ? undefined : T, - _optional: TCheck, ) {} - const _1 = createExternalRouteRef({ params: ['notX'] }); - checkRouteRef(_1, { notX: '' }, false); + const _1 = createExternalRouteRef(); + checkRouteRef(_1, undefined); // @ts-expect-error - checkRouteRef(_1, { x: '' }, false); + checkRouteRef(_1, { x: '' }); - const _2 = createExternalRouteRef({ params: ['x'], optional: true }); - checkRouteRef(_2, { x: '' }, true); + const _2 = createExternalRouteRef({ params: ['x'] }); + checkRouteRef(_2, { x: '' }); // @ts-expect-error - checkRouteRef(_2, undefined, false); + checkRouteRef(_2, { notX: '' }); + // @ts-expect-error + checkRouteRef(_2, undefined); const _3 = createExternalRouteRef({ params: ['x', 'y'] }); - checkRouteRef(_3, { x: '', y: '' }, false); + checkRouteRef(_3, { x: '', y: '' }); // @ts-expect-error - checkRouteRef(_3, { x: '' }, false); + checkRouteRef(_3, { x: '' }); // @ts-expect-error - checkRouteRef(_3, { x: '', y: '', z: '' }, false); + checkRouteRef(_3, { x: '', y: '', z: '' }); const _4 = createExternalRouteRef({ params: [] }); - checkRouteRef(_4, undefined, false); + checkRouteRef(_4, undefined); // @ts-expect-error - checkRouteRef(_4, { x: '' }); - - const _5 = createExternalRouteRef(); - checkRouteRef(_5, undefined, false); - // @ts-expect-error - checkRouteRef(_5, { x: '' }); - - const _6 = createExternalRouteRef({ optional: true }); - checkRouteRef(_6, undefined, true); - // @ts-expect-error - checkRouteRef(_6, undefined, false); + checkRouteRef(_4, { x: '' }); // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); + expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); }); }); diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts index 7a754ffa7b..6ce36707c0 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts @@ -29,18 +29,15 @@ import { AnyRouteRefParams } from './types'; */ export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, > { readonly $$type: '@backstage/ExternalRouteRef'; readonly T: TParams; - readonly optional: TOptional; } /** @internal */ export interface InternalExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, -> extends ExternalRouteRef { +> extends ExternalRouteRef { readonly version: 'v1'; getParams(): string[]; getDescription(): string; @@ -52,11 +49,8 @@ export interface InternalExternalRouteRef< /** @internal */ export function toInternalExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, ->( - resource: ExternalRouteRef, -): InternalExternalRouteRef { - const r = resource as InternalExternalRouteRef; +>(resource: ExternalRouteRef): InternalExternalRouteRef { + const r = resource as InternalExternalRouteRef; if (r.$$type !== '@backstage/ExternalRouteRef') { throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`); } @@ -79,7 +73,6 @@ class ExternalRouteRefImpl readonly $$type = '@backstage/ExternalRouteRef' as any; constructor( - readonly optional: boolean, readonly params: string[] = [], readonly defaultTarget: string | undefined, creationSite: string, @@ -104,7 +97,6 @@ class ExternalRouteRefImpl */ export function createExternalRouteRef< TParams extends { [param in TParamKeys]: string } | undefined = undefined, - TOptional extends boolean = false, TParamKeys extends string = string, >(options?: { /** @@ -114,14 +106,6 @@ export function createExternalRouteRef< ? (keyof TParams)[] : TParamKeys[]; - /** - * Whether or not this route is optional, defaults to false. - * - * Optional external routes are not required to be bound in the app, and - * if they aren't, `useExternalRouteRef` will return `undefined`. - */ - optional?: TOptional; - /** * The route (typically in another plugin) that this should map to by default. * @@ -134,13 +118,11 @@ export function createExternalRouteRef< ? undefined : string extends TParamKeys ? TParams - : { [param in TParamKeys]: string }, - TOptional + : { [param in TParamKeys]: string } > { return new ExternalRouteRefImpl( - Boolean(options?.optional), options?.params as string[] | undefined, options?.defaultTarget, describeParentCallSite(), - ) as ExternalRouteRef; + ); } diff --git a/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts b/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts index 72997e07db..ab5fc2f034 100644 --- a/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts +++ b/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts @@ -16,6 +16,8 @@ const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo'; +// NOTE: This function is also imported and used in backend code + /** * Internal helper that describes the location of the parent caller. * @internal diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx b/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx index 4f43cf8d01..bdac95a5a0 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx @@ -45,7 +45,7 @@ describe('v1 consumer', () => { }); const routeFunc = renderedHook.result.current; - expect(routeFunc()).toBe('/hello'); + expect(routeFunc?.()).toBe('/hello'); expect(resolve).toHaveBeenCalledWith( routeRef, expect.objectContaining({ @@ -54,6 +54,23 @@ describe('v1 consumer', () => { ); }); + it('should ignore missing routes', () => { + const routeRef = createRouteRef(); + + const renderedHook = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + undefined }]]} + > + + + ), + }); + + const routeFunc = renderedHook.result.current; + expect(routeFunc).toBeUndefined(); + }); + it('re-resolves the routeFunc when the search parameters change', () => { const resolve = jest.fn(() => () => '/hello'); diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.tsx b/packages/frontend-plugin-api/src/routing/useRouteRef.tsx index fdf831953d..2d2b3a5458 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.tsx @@ -30,47 +30,14 @@ import { RouteFunc, routeResolutionApiRef, useApi } from '../apis'; * See {@link https://backstage.io/docs/plugins/composability#routing-system} * * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. - * @public - */ -export function useRouteRef< - TOptional extends boolean, - TParams extends AnyRouteRefParams, ->( - routeRef: ExternalRouteRef, -): TOptional extends true ? RouteFunc | undefined : RouteFunc; - -/** - * React hook for constructing URLs to routes. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system} - * - * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. - * @public - */ -export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; - -/** - * React hook for constructing URLs to routes. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system} - * - * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. + * @returns A function that will in turn return the concrete URL of the `routeRef`, or `undefined` if the route is not available. * @public */ export function useRouteRef( routeRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, ): RouteFunc | undefined { const { pathname } = useLocation(); const routeResolutionApi = useApi(routeResolutionApiRef); @@ -80,10 +47,5 @@ export function useRouteRef( [routeResolutionApi, routeRef, pathname], ); - const isOptional = 'optional' in routeRef && routeRef.optional; - if (!routeFunc && !isOptional) { - throw new Error(`No path for ${routeRef}`); - } - return routeFunc; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 327639af37..f9e39e98de 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; @@ -304,9 +305,7 @@ describe('createExtension', () => { baz: z => z.string().optional(), }, }, - output: { - foo: stringDataRef, - }, + output: [stringDataRef], factory({ config }) { const a1: string = config.foo; const a2: string = config.bar; @@ -320,12 +319,10 @@ describe('createExtension', () => { const c3: number = config.baz; unused(a1, a2, a3, c1, c2, c3); - return { - foo: 'bar', - }; + return [stringDataRef('bar')]; }, }); - expect(extension).toMatchObject({ version: 'v1', namespace: 'test' }); + expect(extension).toMatchObject({ version: 'v2', namespace: 'test' }); expect(String(extension)).toBe( 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); @@ -394,6 +391,18 @@ describe('createExtension', () => { }), ).toMatchObject({ version: 'v2' }); + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + factory() { + return [stringDataRef('hello'), numberDataRef(4)]; + }, + }), + ).toMatchObject({ version: 'v2' }); + expect( createExtension({ namespace: 'test', @@ -428,6 +437,93 @@ describe('createExtension', () => { ).toMatchObject({ version: 'v2' }); }); + it('should support new form of outputs with a generator', () => { + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + // Missing all outputs + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + yield stringDataRef('hello'); // Missing number output + }, + }), + ).toMatchObject({ version: 'v2' }); + + // Duplicate output, we won't attempt to handle this a compile time and instead error out at runtime + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + *factory() { + yield stringDataRef('hello'); + yield stringDataRef('hello'); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); // No declared output + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + *factory() { + yield stringDataRef('hello'); // Missing number output, but it's optional so that's allowed + }, + }), + ).toMatchObject({ version: 'v2' }); + }); + it('should support new form of inputs', () => { expect( createExtension({ @@ -462,4 +558,179 @@ describe('createExtension', () => { }), ).toMatchObject({ version: 'v2' }); }); + + describe('overrides', () => { + it('should allow overriding of config and merging', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + config: { + schema: { + bar: z => z.string().optional(), + }, + }, + factory(_, { config }) { + return [stringDataRef(config.foo ?? config.bar ?? 'default')]; + }, + }); + + expect(true).toBe(true); + }); + + it('should allow overriding of outputs', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + inputs: { + test: createExtensionInput([stringDataRef], { + singleton: true, + }), + }, + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory({ inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + const override1 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [numberDataRef(inputs.test.get(stringDataRef).length)]; + }, + }); + + // @ts-expect-error - this should fail because string output should be merged? + const override2 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + unused(override1, override2); + + expect(true).toBe(true); + }); + + it('should allow overriding the factory function and calling the original factory', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + factory(originalFactory) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + // @ts-expect-error - fails because original factory does not return number + const number: boolean = response.get(numberDataRef); + + return [stringDataRef(`foo-${foo}-override`)]; + }, + }); + + expect(true).toBe(true); + }); + + it('should allow overriding the returned values from the parent factory', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo), numberDataRef(42)]; + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output; + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const tester = createExtensionTester(overridden); + + expect(tester.data(numberDataRef)).toBe(43); + }); + + it('should work functionally with overrides', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo)]; + }, + }); + + const overriden = testExtension.override({ + config: { + schema: { + bar: z => z.string().default('hello'), + }, + }, + factory(originalFactory, { config }) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + return [stringDataRef(`foo-${foo}-override-${config.bar}`)]; + }, + }); + + expect(createExtensionTester(overriden).data(stringDataRef)).toBe( + 'foo-boom-override-hello', + ); + + expect( + createExtensionTester(overriden, { + config: { foo: 'hello', bar: 'world' }, + }).data(stringDataRef), + ).toBe('foo-hello-override-world'); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 4d31982ed3..f2d3d906ab 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -17,6 +17,7 @@ import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; +import { createDataContainer } from './createExtensionBlueprint'; import { AnyExtensionDataRef, ExtensionDataRef, @@ -65,7 +66,17 @@ export type ExtensionDataValues = { /** @public */ export type ExtensionDataContainer = - { + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { get( ref: ExtensionDataRef, ): UExtensionData extends ExtensionDataRef @@ -117,7 +128,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, > { kind?: string; namespace?: string; @@ -126,21 +136,10 @@ export interface LegacyCreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: TOutput; - /** @deprecated - use `config.schema` instead */ configSchema?: PortableSchema; - config?: { - schema: TConfigSchema; - }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: TConfig; inputs: Expand>; }): Expand>; } @@ -157,9 +156,16 @@ export type VerifyExtensionFactoryOutput< : never ) extends infer IRequiredOutputIds ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? {} + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} + : { + 'Error: The extension factory has undeclared output(s)': Exclude< + UFactoryOutput['id'], + UDeclaredOutput['id'] + >; + } : { - 'Error: The extension factory is missing the following outputs': Exclude< + 'Error: The extension factory is missing the following output(s)': Exclude< IRequiredOutputIds, UFactoryOutput['id'] >; @@ -168,6 +174,9 @@ export type VerifyExtensionFactoryOutput< /** @public */ export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -175,83 +184,174 @@ export type CreateExtensionOptions< { optional: boolean; singleton: boolean } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { - kind?: string; - namespace?: string; - name?: string; + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; - /** @deprecated - use `config.schema` instead */ - configSchema?: PortableSchema; config?: { schema: TConfigSchema; }; factory(context: { node: AppNode; - config: TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }); + config: { + [key in keyof TConfigSchema]: z.infer>; + }; inputs: Expand>; }): Iterable; } & VerifyExtensionFactoryOutput; /** @public */ -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> { $$type: '@backstage/ExtensionDefinition'; - readonly kind?: string; - readonly namespace?: string; - readonly name?: string; + readonly kind?: TKind; + readonly namespace?: TNamespace; + readonly name?: TName; readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema; + + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >( + args: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >, + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + TNamespace, + TName + >; } /** @internal */ -export type InternalExtensionDefinition = - ExtensionDefinition & - ( - | { - readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; - readonly output: AnyExtensionDataMap; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs; - }): ExtensionDataValues; - } - | { - readonly version: 'v2'; - readonly inputs: { +export type InternalExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> = ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + TNamespace, + TName +> & + ( + | { + readonly version: 'v1'; + readonly inputs: AnyExtensionInputMap; + readonly output: AnyExtensionDataMap; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs; + }): ExtensionDataValues; + } + | { + readonly version: 'v2'; + readonly inputs: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }; + readonly output: Array; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< AnyExtensionDataRef, { optional: boolean; singleton: boolean } >; - }; - readonly output: Array; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }>; - }): Iterable>; - } - ); + }>; + }): Iterable>; + } + ); /** @internal */ export function toInternalExtensionDefinition( @@ -284,34 +384,35 @@ export function createExtension< { optional: boolean; singleton: boolean } >; }, - TConfig, - TConfigInput, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, >( options: CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput >, ): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName >; /** * @public @@ -322,32 +423,18 @@ export function createExtension< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, >( options: LegacyCreateExtensionOptions< TOutput, TInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, -): ExtensionDefinition< - TConfig & - (string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }), - TConfigInput & - (string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >) ->; +): ExtensionDefinition; export function createExtension< + const TKind extends string | undefined, + const TNamespace extends string | undefined, + const TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -363,10 +450,11 @@ export function createExtension< >( options: | CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput > @@ -374,8 +462,7 @@ export function createExtension< AnyExtensionDataMap, TLegacyInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, ): ExtensionDefinition< TConfig & @@ -391,21 +478,32 @@ export function createExtension< z.ZodObject<{ [key in keyof TConfigSchema]: ReturnType; }> - >) + >), + UOutput, + TInputs, + TKind, + TNamespace, + TName > { - const newConfigSchema = options.config?.schema; - if (newConfigSchema && options.configSchema) { + if ('configSchema' in options && 'config' in options) { throw new Error(`Cannot provide both configSchema and config.schema`); } - const configSchema = newConfigSchema - ? createSchemaFromZod(innerZ => + let configSchema: PortableSchema | undefined; + if ('configSchema' in options) { + configSchema = options.configSchema; + } + if ('config' in options) { + const newConfigSchema = options.config?.schema; + configSchema = + newConfigSchema && + createSchemaFromZod(innerZ => innerZ.object( Object.fromEntries( Object.entries(newConfigSchema).map(([k, v]) => [k, v(innerZ)]), ), ), - ) - : options.configSchema; + ); + } return { $$type: '@backstage/ExtensionDefinition', @@ -433,6 +531,147 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, + override: < + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UOverrideFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >(overrideOptions: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs; + output?: Array; + config?: { + schema: TExtensionConfigSchema; + }; + factory( + originalFactory: (context?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + }): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject< + { + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + } & { + [key in keyof TConfigSchema]: ReturnType; + } + > + >, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + TNamespace, + TName + > => { + if (!Array.isArray(options.output)) { + throw new Error( + 'Cannot override an extension that is not declared using the new format with outputs as an array', + ); + } + const newOptions = options as CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + >; + const overrideNewConfigSchema = overrideOptions.config?.schema; + + const schema = { + ...newOptions.config?.schema, + ...overrideNewConfigSchema, + } as TConfigSchema & TExtensionConfigSchema; + + return createExtension({ + kind: newOptions.kind, + namespace: newOptions.namespace, + name: newOptions.name, + attachTo: overrideOptions.attachTo ?? newOptions.attachTo, + disabled: overrideOptions.disabled ?? newOptions.disabled, + inputs: { ...overrideOptions.inputs, ...newOptions.inputs }, + output: overrideOptions.output ?? newOptions.output, + config: Object.keys(schema).length === 0 ? undefined : { schema }, + factory: ({ node, config, inputs }) => { + if (!overrideOptions.factory) { + return newOptions.factory({ + node, + config, + inputs: inputs as unknown as Expand< + ResolvedExtensionInputs + >, + }); + } + const parentResult = overrideOptions.factory( + (innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }): ExtensionDataContainer => { + return createDataContainer( + newOptions.factory({ + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }) as Iterable, + ); + }, + { + node, + config, + inputs, + }, + ); + + const deduplicatedResult = new Map(); + for (const item of parentResult) { + deduplicatedResult.set(item.id, item); + } + + return deduplicatedResult.values() as Iterable; + }, + } as CreateExtensionOptions); + }, } as InternalExtensionDefinition< TConfig & (string extends keyof TConfigSchema @@ -449,6 +688,11 @@ export function createExtension< z.ZodObject<{ [key in keyof TConfigSchema]: ReturnType; }> - >) + >), + UOutput, + TInputs, + TKind, + TNamespace, + TName >; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index f57cc2b70e..b9d1475074 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -21,9 +21,21 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; +import { + ExtensionDefinition, + toInternalExtensionDefinition, +} from './createExtension'; function unused(..._any: any[]) {} +function factoryOutput(ext: ExtensionDefinition) { + const int = toInternalExtensionDefinition(ext); + if (int.version !== 'v2') { + throw new Error('Expected v2 extension'); + } + return Array.from(int.factory({} as any)); +} + describe('createExtensionBlueprint', () => { it('should allow creation of extension blueprints', () => { const TestExtensionBlueprint = createExtensionBlueprint({ @@ -57,6 +69,47 @@ describe('createExtensionBlueprint', () => { output: [coreExtensionData.reactElement], factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), + version: 'v2', + }); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + }); + + it('should allow creation of extension blueprints with a generator', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + *factory(params: { text: string }) { + yield coreExtensionData.reactElement(

{params.text}

); + }, + }); + + const extension = TestExtensionBlueprint.make({ + name: 'my-extension', + params: { + text: 'Hello, world!', + }, + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + attachTo: { + id: 'test', + input: 'default', + }, + configSchema: undefined, + disabled: false, + inputs: {}, + kind: 'test-extension', + name: 'my-extension', + namespace: undefined, + output: [coreExtensionData.reactElement], + factory: expect.any(Function), + toString: expect.any(Function), + override: expect.any(Function), version: 'v2', }); @@ -74,7 +127,7 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', factory(origFactory) { return origFactory({ @@ -132,11 +185,8 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', - params: { - text: 'Hello, world!', - }, config: { schema: { something: z => z.string(), @@ -183,7 +233,7 @@ describe('createExtensionBlueprint', () => { }, }); - TestExtensionBlueprint.make({ + TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', params: { text: 'Hello, world!', @@ -213,11 +263,8 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', - params: { - text: 'Hello, world!', - }, config: { schema: { something: z => z.string(), @@ -287,4 +334,203 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should allow merging of inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement.optional()], + factory(_params: { x?: string }, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + unused(ref); + return []; + }, + }); + + blueprint.makeWithOverrides({ + inputs: { + test2: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + factory(origFactory, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + const el: JSX.Element = inputs.test2.get( + coreExtensionData.reactElement, + ); + + unused(ref, el); + + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); + + it('should not allow overriding inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef]), + }, + output: [coreExtensionData.reactElement.optional()], + factory() { + return []; + }, + }); + + blueprint.makeWithOverrides({ + inputs: { + // @ts-expect-error + test: createExtensionInput([]), // Overrides are not allowed + }, + factory(origFactory) { + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); + + it('should replace the outputs when provided through make', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + const ext = toInternalExtensionDefinition( + blueprint.makeWithOverrides({ + output: [testDataRef2], + factory(origFactory) { + const parent = origFactory({}); + return [testDataRef2(`${parent.get(testDataRef1)}bar`)]; + }, + }), + ); + + expect(ext.output).toEqual([testDataRef2]); + + expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]); + }); + + it('should allow returning of the parent data container', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + factory(origFactory) { + return [...origFactory({}), testDataRef2('bar')]; + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); + }); + + it('should not allow returning parent output if outputs are overridden', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.makeWithOverrides({ + output: [testDataRef2.optional()], + // @ts-expect-error + *factory() { + yield testDataRef1('foo'); + yield testDataRef2('bar'); + }, + }); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef2.optional()], + *factory() { + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef2('bar')]); + }); + + it('should not rely on optional outputs when forwarding from parent', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1, testDataRef2.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + // @ts-expect-error + *factory(origFactory) { + yield* origFactory({}); + }, + }); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index b579cc207a..656e681a2b 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -18,6 +18,7 @@ import { AppNode } from '../apis'; import { Expand } from '../types'; import { CreateExtensionOptions, + ExtensionDataContainer, ExtensionDefinition, ResolvedExtensionInputs, VerifyExtensionFactoryOutput, @@ -27,7 +28,7 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, - ExtensionDataRefToValue, + ExtensionDataRef, ExtensionDataValue, } from './createExtensionDataRef'; @@ -35,6 +36,9 @@ import { * @public */ export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -47,12 +51,13 @@ export type CreateExtensionBlueprintOptions< UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { - kind: string; - namespace?: string; + kind: TKind; + namespace?: TNamespace; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; + name?: TName; config?: { schema: TConfigSchema; }; @@ -74,6 +79,9 @@ export type CreateExtensionBlueprintOptions< * @public */ export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -82,67 +90,90 @@ export interface ExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown }, TConfigInput extends { [key in string]: unknown }, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > { dataRefs: TDataRefs; + make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + /** * Creates a new extension from the blueprint. * * You must either pass `params` directly, or define a `factory` that can * optionally call the original factory with the same params. */ - make< + makeWithOverrides< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, - >( - args: { - namespace?: string; - name?: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; - }; + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; }; - } & ( - | ({ - factory( - originalFactory: ( - params: TParams, - context?: { - config?: TConfig; - inputs?: Expand>; - }, - ) => Iterable>, - context: { - node: AppNode; - config: TConfig & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & VerifyExtensionFactoryOutput< - UOutput & UExtraOutput, - UFactoryOutput - >) - | { - params: TParams; - } - ), - ): ExtensionDefinition< + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -155,14 +186,46 @@ export interface ExtensionBlueprint< >; }> > & - TConfigInput + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName >; } +/** @internal */ +export function createDataContainer( + values: Iterable< + UData extends ExtensionDataRef + ? ExtensionDataValue + : never + >, +): ExtensionDataContainer { + const container = new Map>(); + + for (const output of values) { + container.set(output.id, output); + } + + return { + get(ref) { + return container.get(ref.id)?.value; + }, + [Symbol.iterator]() { + return container.values(); + }, + } as ExtensionDataContainer; +} + /** * @internal */ class ExtensionBlueprintImpl< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -171,12 +234,14 @@ class ExtensionBlueprintImpl< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > { constructor( private readonly options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -190,23 +255,31 @@ class ExtensionBlueprintImpl< dataRefs: TDataRefs; - public make< + public makeWithOverrides< TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, >(args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TInputs; - output?: Array; - params?: TParams; + inputs?: TExtraInputs; + output?: Array; config?: { schema: TExtensionConfigSchema; }; - factory?( + factory( originalFactory: ( params: TParams, context?: { @@ -217,7 +290,7 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: { @@ -227,7 +300,7 @@ class ExtensionBlueprintImpl< } & { [key in keyof TConfigSchema]: z.infer>; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; }): ExtensionDefinition< @@ -254,74 +327,77 @@ class ExtensionBlueprintImpl< ...this.options.config?.schema, ...args.config?.schema, } as TConfigSchema & TExtensionConfigSchema; + return createExtension({ kind: this.options.kind, namespace: args.namespace ?? this.options.namespace, - name: args.name, + name: args.name ?? this.options.name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, - inputs: args.inputs ?? this.options.inputs, - output: [...(args.output ?? []), ...this.options.output], + inputs: { ...args.inputs, ...this.options.inputs }, + output: args.output ?? this.options.output, config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { - if (args.factory) { - return args.factory( - ( - innerParams: TParams, - innerContext?: { - config?: { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }; - inputs?: Expand>; - }, - ): Iterable> => { - return this.options.factory(innerParams, { + return args.factory( + ( + innerParams: TParams, + innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }, + ): ExtensionDataContainer => { + return createDataContainer( + this.options.factory(innerParams, { node, config: innerContext?.config ?? config, - inputs: innerContext?.inputs ?? inputs, - }); - }, - { - node, - config, - inputs, - }, - ); - } else if (args.params) { - return this.options.factory(args.params, { + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }), + ); + }, + { node, config, inputs, - }); - } - throw new Error('Either params or factory must be provided'); + }, + ); }, - } as CreateExtensionOptions< - UOutput, - TInputs, - { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - } & { - [key in keyof TConfigSchema]: z.infer>; - }, - z.input< - z.ZodObject< - { - [key in keyof TExtensionConfigSchema]: ReturnType< - TExtensionConfigSchema[key] - >; - } & { - [key in keyof TConfigSchema]: ReturnType; - } - > - >, - TConfigSchema, - UFactoryOutput - >); + } as CreateExtensionOptions); + } + + public make< + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + > + > { + return createExtension({ + kind: this.options.kind, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: this.options.inputs, + output: this.options.output, + config: this.options.config, + factory: ctx => this.options.factory(args.params, ctx), + } as CreateExtensionOptions); } } @@ -340,12 +416,17 @@ export function createExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, >( options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -354,10 +435,12 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, - TInputs, - UExtraOutput, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { [key in keyof TConfigSchema]: z.infer> }, @@ -371,10 +454,12 @@ export function createExtensionBlueprint< TDataRefs > { return new ExtensionBlueprintImpl(options) as ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, - TInputs, - UExtraOutput, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index bb9bdc2107..76cc030be3 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -34,11 +34,9 @@ const nameExtensionDataRef = createExtensionDataRef().with({ const Extension1 = createExtension({ name: '1', attachTo: { id: 'test/output', input: 'names' }, - output: { - name: nameExtensionDataRef, - }, + output: [nameExtensionDataRef], factory() { - return { name: 'extension-1' }; + return [nameExtensionDataRef('extension-1')]; }, }); @@ -150,6 +148,10 @@ describe('createPlugin', () => { }); expect(plugin).toBeDefined(); + expect(plugin.getExtension('test/1')).toBe(Extension1); + // @ts-expect-error + expect(plugin.getExtension('nonexistent')).toBeUndefined(); + await renderWithEffects( createTestAppRoot({ features: [plugin], diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index b671d95d2f..1d2a12ffe2 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -17,6 +17,7 @@ import { ExtensionDefinition } from './createExtension'; import { Extension, + ResolveExtensionId, resolveExtensionDefinition, } from './resolveExtensionDefinition'; import { @@ -28,21 +29,23 @@ import { /** @public */ export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { - id: string; - routes?: Routes; - externalRoutes?: ExternalRoutes; - extensions?: ExtensionDefinition[]; + id: TId; + routes?: TRoutes; + externalRoutes?: TExternalRoutes; + extensions?: TExtensions; featureFlags?: FeatureFlagConfig[]; } /** @public */ export interface InternalBackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, -> extends BackstagePlugin { + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, +> extends BackstagePlugin { readonly version: 'v1'; readonly extensions: Extension[]; readonly featureFlags: FeatureFlagConfig[]; @@ -50,17 +53,36 @@ export interface InternalBackstagePlugin< /** @public */ export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin { - const extensions = (options.extensions ?? []).map(def => - resolveExtensionDefinition(def, { namespace: options.id }), - ); + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +> { + const extensions = new Array>(); + const extensionDefinitionsById = new Map< + string, + ExtensionDefinition + >(); - const extensionIds = extensions.map(e => e.id); - if (extensionIds.length !== new Set(extensionIds).size) { + for (const def of options.extensions ?? []) { + const ext = resolveExtensionDefinition(def, { namespace: options.id }); + extensions.push(ext); + extensionDefinitionsById.set(ext.id, def); + } + + if (extensions.length !== extensionDefinitionsById.size) { + const extensionIds = extensions.map(e => e.id); const duplicates = Array.from( new Set( extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index), @@ -78,14 +100,17 @@ export function createPlugin< $$type: '@backstage/BackstagePlugin', version: 'v1', id: options.id, - routes: options.routes ?? ({} as Routes), - externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes), + routes: options.routes ?? ({} as TRoutes), + externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes), featureFlags: options.featureFlags ?? [], extensions, + getExtension(id) { + return extensionDefinitionsById.get(id); + }, toString() { return `Plugin{id=${options.id}}`; }, - } as InternalBackstagePlugin; + } as InternalBackstagePlugin; } /** @internal */ diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index e1bfcbfc11..5a42e9100e 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -15,7 +15,10 @@ */ import { ExtensionDefinition } from './createExtension'; -import { resolveExtensionDefinition } from './resolveExtensionDefinition'; +import { + ResolveExtensionId, + resolveExtensionDefinition, +} from './resolveExtensionDefinition'; describe('resolveExtensionDefinition', () => { const baseDef = { @@ -23,6 +26,7 @@ describe('resolveExtensionDefinition', () => { version: 'v2', attachTo: { id: '', input: '' }, disabled: false, + override: () => ({} as ExtensionDefinition), }; it.each([ @@ -63,6 +67,7 @@ describe('old resolveExtensionDefinition', () => { version: 'v1', attachTo: { id: '', input: '' }, disabled: false, + override: () => ({} as ExtensionDefinition), }; it.each([ @@ -96,3 +101,103 @@ describe('old resolveExtensionDefinition', () => { ); }); }); + +describe('ResolveExtensionId', () => { + it('should resolve extension IDs correctly', () => { + type NamedExtension< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, + > = ExtensionDefinition; + + const id1: 'k:ns' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + undefined + >; + const id2: 'k:n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + undefined + >; + const id3: 'ns/n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id4: never = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + undefined + >; + const id5: 'ns' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id6: 'n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id7: 'k:ns/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + undefined + >; + const id8: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + 'ns2' + >; + const id9: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + 'ns2' + >; + const ida: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idb: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + 'ns2' + >; + const idc: 'ns2' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idd: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const ide: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + 'ns2' + >; + + const invalid1: never = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const invalid2: never = {} as ResolveExtensionId< + NamedExtension<'k', string | undefined, 'n'>, + undefined + >; + const invalid3: never = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', string | undefined>, + undefined + >; + + expect([ + id1, + id2, + id3, + id4, + id5, + id6, + id7, + id8, + id9, + ida, + idb, + idc, + idd, + ide, + invalid1, + invalid2, + invalid3, + ]).toBeDefined(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 18b6f0a653..35bf9c6b05 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -94,13 +94,49 @@ export function toInternalExtension( return internal; } +/** @ignore */ +export type ResolveExtensionId< + TExtension extends ExtensionDefinition, + TDefaultNamespace extends string | undefined, +> = TExtension extends ExtensionDefinition< + any, + any, + any, + any, + infer IKind, + infer INamespace, + infer IName +> + ? [string | undefined] extends [IKind | INamespace | IName] + ? never + : ( + ( + undefined extends TDefaultNamespace ? INamespace : TDefaultNamespace + ) extends infer ISelectedNamespace extends string + ? undefined extends IName + ? ISelectedNamespace + : `${ISelectedNamespace}/${IName}` + : IName + ) extends infer INamePart extends string + ? IKind extends string + ? `${IKind}:${INamePart}` + : INamePart + : never + : never; + /** @internal */ export function resolveExtensionDefinition( definition: ExtensionDefinition, context?: { namespace?: string }, ): Extension { const internalDefinition = toInternalExtensionDefinition(definition); - const { name, kind, namespace: _, ...rest } = internalDefinition; + const { + name, + kind, + namespace: _skip1, + override: _skip2, + ...rest + } = internalDefinition; const namespace = internalDefinition.namespace ?? context?.namespace; const namePart = diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index 7e0650e06c..b1afd4e7ea 100644 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -15,6 +15,7 @@ */ import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing'; +import { ExtensionDefinition } from './createExtension'; /** * Feature flag configuration. @@ -32,15 +33,24 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef }; /** @public */ export type AnyExternalRoutes = { [name in string]: ExternalRouteRef }; +/** @public */ +export type ExtensionMap< + TExtensionMap extends { [id in string]: ExtensionDefinition }, +> = { + get(id: TId): TExtensionMap[TId]; +}; + /** @public */ export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { [id in string]: ExtensionDefinition } = {}, > { readonly $$type: '@backstage/BackstagePlugin'; readonly id: string; - readonly routes: Routes; - readonly externalRoutes: ExternalRoutes; + readonly routes: TRoutes; + readonly externalRoutes: TExternalRoutes; + getExtension(id: TId): TExtensionMap[TId]; } /** @public */ diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 8f770b7b31..4ba38f5065 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/frontend-test-utils +## 0.1.12-next.2 + +### Patch Changes + +- 8209449: Added new APIs for testing extensions +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.12-next.1 ### Patch Changes diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 3b368394ca..3aa9069219 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -7,7 +7,10 @@ import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; +import { AppNode } from '@backstage/frontend-plugin-api'; +import { AppNodeInstance } from '@backstage/frontend-plugin-api'; import { ErrorWithContext } from '@backstage/test-utils'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { MockConfigApi } from '@backstage/test-utils'; @@ -36,6 +39,17 @@ export function createExtensionTester( export { ErrorWithContext }; +// @public (undocumented) +export class ExtensionQuery { + constructor(node: AppNode); + // (undocumented) + data(ref: ExtensionDataRef): T | undefined; + // (undocumented) + get instance(): AppNodeInstance; + // (undocumented) + get node(): AppNode; +} + // @public (undocumented) export class ExtensionTester { // (undocumented) @@ -46,6 +60,10 @@ export class ExtensionTester { }, ): ExtensionTester; // (undocumented) + data(ref: ExtensionDataRef): T | undefined; + // (undocumented) + query(id: string | ExtensionDefinition): ExtensionQuery; + // (undocumented) render(options?: { config?: JsonObject }): RenderResult; } diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 218a6fa4c4..e8152cfdda 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.1.12-next.1", + "version": "0.1.12-next.2", "backstage": { "role": "web-library" }, @@ -31,6 +31,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/config": "workspace:^", "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index f59d44f6d7..889c815fc7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -24,6 +24,8 @@ import { createApiExtension, createApiFactory, createExtension, + createExtensionDataRef, + createExtensionInput, createSchemaFromZod, useAnalytics, useApi, @@ -31,6 +33,10 @@ import { import { MockAnalyticsApi } from '../apis'; import { createExtensionTester } from './createExtensionTester'; +const stringDataRef = createExtensionDataRef().with({ + id: 'test.string', +}); + describe('createExtensionTester', () => { const defaultDefinition = { namespace: 'test', @@ -229,4 +235,133 @@ describe('createExtensionTester', () => { ), ); }); + + it('should return the correct dataRef when called', () => { + const extension = createExtension({ + namespace: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(stringDataRef)).toBe('test-text'); + }); + + it('should throw an error if trying to access an instance not provided to the tester', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension); + + expect(() => tester.query(extension2)).toThrow( + "Extension with ID 'test/e2' not found, please make sure it's added to the tester", + ); + }); + + it('should throw an error if trying to access an instance which is not part of the tree', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension).add(extension2); + + expect(() => tester.query(extension2)).toThrow( + "Extension with ID 'test/e2' has not been instantiated, because it is not part of the test subject's extension tree", + ); + }); + + // TODO: this should be implemented + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should allow querying an extension and getting outputs', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + inputs: { + input: createExtensionInput( + { + output: stringDataRef, + }, + { singleton: true }, + ), + }, + factory: ({ inputs }) => ({ + text: `nest-${inputs.input.output.output}`, + }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'test/e1', input: 'blob' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension).add(extension2); + + expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension2).data(stringDataRef)).toBe('test-text'); + // @ts-expect-error + expect(tester.query(extension).input('input').data(stringDataRef)).toBe( + 'nest-test-text', + ); + }); + + // TODO: this should be implemented + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should allow defining inputs to extensions without a corresponding extension definition', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + inputs: { + input: createExtensionInput( + { + output: stringDataRef, + }, + { singleton: true }, + ), + }, + factory: ({ inputs }) => ({ + text: `nest-${inputs.input.output.output}`, + }), + }); + + const tester = createExtensionTester(extension, { + // @ts-expect-error + inputs: { input: 'test-text' }, + }); + + expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 81b74c9fce..4bfc8392cb 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -19,7 +19,10 @@ import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { - ExtensionDataValue, + AppNode, + AppTree, + Extension, + ExtensionDataRef, ExtensionDefinition, IconComponent, RouteRef, @@ -31,12 +34,20 @@ import { createRouterExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; -import { MockConfigApi } from '@backstage/test-utils'; +import { Config, ConfigReader } from '@backstage/config'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/createExtension'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { resolveAppTree } from '../../../frontend-app-api/src/tree/resolveAppTree'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveAppNodeSpecs'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; const NavItem = (props: { routeRef: RouteRef; @@ -44,10 +55,13 @@ const NavItem = (props: { icon: IconComponent; }) => { const { routeRef, title, icon: Icon } = props; - const to = useRouteRef(routeRef)(); + const link = useRouteRef(routeRef); + if (!link) { + return null; + } return (
  • - + {title}
  • @@ -86,6 +100,35 @@ const TestAppNavExtension = createExtension({ }, }); +/** @public */ +export class ExtensionQuery { + #node: AppNode; + + constructor(node: AppNode) { + this.#node = node; + } + + get node() { + return this.#node; + } + + get instance() { + const instance = this.#node.instance; + if (!instance) { + throw new Error( + `Unable to access the instance of extension with ID '${ + this.#node.spec.id + }'`, + ); + } + return instance; + } + + data(ref: ExtensionDataRef): T | undefined { + return this.instance.getData(ref); + } +} + /** @public */ export class ExtensionTester { /** @internal */ @@ -125,9 +168,7 @@ export class ExtensionTester { : [...internal.output, coreExtensionData.routePath], factory: params => { const parentOutput = Array.from( - internal.factory(params) as Iterable< - ExtensionDataValue - >, + internal.factory(params as any), ).filter(val => val.id !== coreExtensionData.routePath.id); return [...parentOutput, coreExtensionData.routePath('/')]; @@ -139,8 +180,11 @@ export class ExtensionTester { return tester; } + #tree?: AppTree; + readonly #extensions = new Array<{ id: string; + extension: Extension; definition: ExtensionDefinition; config?: JsonValue; }>(); @@ -149,6 +193,12 @@ export class ExtensionTester { extension: ExtensionDefinition, options?: { config?: TConfigInput }, ): ExtensionTester { + if (this.#tree) { + throw new Error( + 'Cannot add more extensions accessing the extension tree', + ); + } + const { name, namespace } = extension; const definition = { @@ -157,10 +207,11 @@ export class ExtensionTester { name: !namespace && !name ? 'test' : name, }; - const { id } = resolveExtensionDefinition(definition); + const resolvedExtension = resolveExtensionDefinition(definition); this.#extensions.push({ - id, + id: resolvedExtension.id, + extension: resolvedExtension, definition, config: options?.config as JsonValue, }); @@ -168,38 +219,42 @@ export class ExtensionTester { return this; } + data(ref: ExtensionDataRef): T | undefined { + const tree = this.#resolveTree(); + + return new ExtensionQuery(tree.root).data(ref); + } + + query(id: string | ExtensionDefinition): ExtensionQuery { + const tree = this.#resolveTree(); + + const actualId = + typeof id === 'string' ? id : resolveExtensionDefinition(id).id; + + const node = tree.nodes.get(actualId); + + if (!node) { + throw new Error( + `Extension with ID '${actualId}' not found, please make sure it's added to the tester.`, + ); + } else if (!node.instance) { + throw new Error( + `Extension with ID '${actualId}' has not been instantiated, because it is not part of the test subject's extension tree.`, + ); + } + return new ExtensionQuery(node); + } + render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; - const [subject, ...rest] = this.#extensions; + const [subject] = this.#extensions; if (!subject) { throw new Error( 'No subject found. At least one extension should be added to the tester.', ); } - const extensionsConfig: JsonArray = [ - ...rest.map(extension => ({ - [extension.id]: { - config: extension.config, - }, - })), - { - [subject.id]: { - config: subject.config, - disabled: false, - }, - }, - ]; - - const finalConfig = { - ...config, - app: { - ...(typeof config.app === 'object' ? config.app : undefined), - extensions: extensionsConfig, - }, - }; - const app = createSpecializedApp({ features: [ createExtensionOverrides({ @@ -215,11 +270,69 @@ export class ExtensionTester { ], }), ], - config: new MockConfigApi(finalConfig), + config: this.#getConfig(config), }); return render(app.createRoot()); } + + #resolveTree() { + if (this.#tree) { + return this.#tree; + } + + const [subject] = this.#extensions; + if (!subject) { + throw new Error( + 'No subject found. At least one extension should be added to the tester.', + ); + } + + const tree = resolveAppTree( + subject.id, + resolveAppNodeSpecs({ + features: [], + builtinExtensions: this.#extensions.map(_ => _.extension), + parameters: readAppExtensionsConfig(this.#getConfig()), + }), + ); + + instantiateAppNodeTree(tree.root); + + this.#tree = tree; + + return tree; + } + + #getConfig(additionalConfig?: JsonObject): Config { + const [subject, ...rest] = this.#extensions; + + const extensionsConfig: JsonArray = [ + ...rest.map(extension => ({ + [extension.id]: { + config: extension.config, + }, + })), + { + [subject.id]: { + config: subject.config, + disabled: false, + }, + }, + ]; + + return ConfigReader.fromConfigs([ + { context: 'render-config', data: additionalConfig ?? {} }, + { + context: 'test', + data: { + app: { + extensions: extensionsConfig, + }, + }, + }, + ]); + } } /** @public */ diff --git a/packages/frontend-test-utils/src/app/index.ts b/packages/frontend-test-utils/src/app/index.ts index 2351561022..6c6ef26e4c 100644 --- a/packages/frontend-test-utils/src/app/index.ts +++ b/packages/frontend-test-utils/src/app/index.ts @@ -17,6 +17,7 @@ export { createExtensionTester, type ExtensionTester, + type ExtensionQuery, } from './createExtensionTester'; export { renderInTestApp, type TestAppOptions } from './renderInTestApp'; diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 35c7f35836..f74dd57ea0 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/repo-tools +## 0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.9.5-next.1 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 7e03d78758..63be4d526e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.9.5-next.1", + "version": "0.9.5-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 728145048a..57fb204390 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,24 @@ # techdocs-cli-embedded-app +## 0.2.99-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 0.2.99-next.2 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 09635b74ed..472936193a 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.99-next.2", + "version": "0.2.99-next.3", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index d6d5128e4d..76f9891bf0 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,16 @@ # @techdocs/cli +## 1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + ## 1.8.17-next.1 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index fc28ab1458..c655649ceb 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.8.17-next.1", + "version": "1.8.17-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 6684b8b4b5..a6c013a34a 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/test-utils +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + ## 1.5.10-next.1 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 32a69da9df..3007c734bd 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.5.10-next.1", + "version": "1.5.10-next.2", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts index 6c7157ebe9..9ac3401041 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts @@ -207,4 +207,87 @@ describe('BackstageResolver', () => { expect(getManifestByVersionMock).toHaveBeenCalledTimes(2); }); }); + + describe('getSatisfying', () => { + it('filters out locators for other packages', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + {}, + [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'foo'), + 'npm:1.2.3', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'bar'), + 'npm:1.2.3', + ), + ], + ), + ).resolves.toEqual({ + locators: [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + ], + sorted: true, + }); + }); + + it('filters out locators for other package versions', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + {}, + [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:5.6.7', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'bar'), + 'npm:7.8.9', + ), + ], + ), + ).resolves.toEqual({ + locators: [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + ], + sorted: true, + }); + }); + + it('throws for non `backstage:` descriptors', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'npm:1.2.3', + ), + {}, + [], + ), + ).rejects.toThrow(/unexpected npm: range/i); + }); + }); }); diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts index 33bba5df53..82e6c013a3 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.ts @@ -92,6 +92,29 @@ export class BackstageResolver implements Resolver { ]; } + /** + * Given a descriptor and a list of possible locators, return a filtered list + * containing only locators that satisfy the descriptor. Since each Backstage + * release version corresponds to a single version for each package, we just + * need to filter that array for locators with that exact version. + */ + async getSatisfying( + descriptor: Descriptor, + _dependencies: Record, + locators: Array, + ): Promise<{ locators: Locator[]; sorted: boolean }> { + const packageVersion = await getPackageVersion(descriptor); + + return { + locators: locators.filter( + locator => + structUtils.areIdentsEqual(descriptor, locator) && + locator.reference === `npm:${packageVersion}`, + ), + sorted: true, + }; + } + /** * This plugin does not need to support any locators itself, since the * `getCandidates` method will always convert `backstage:` versions into @@ -106,14 +129,6 @@ export class BackstageResolver implements Resolver { */ getResolutionDependencies = () => ({}); - /** - * Candidate versions produced by this resolver always use the `npm:` - * protocol, so this function will never be called. - */ - async getSatisfying(): Promise<{ locators: Locator[]; sorted: boolean }> { - throw new Error('Unreachable'); - } - /** * Once transformed into locators (through getCandidates), the versions are * resolved by the NpmSemverResolver diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index f649d4e0b8..c87bfcaafa 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-api-docs +## 0.11.8-next.2 + +### Patch Changes + +- 4b6d2cb: Updated dependency `@graphiql/react` to `^0.23.0`. +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.11.8-next.1 ### Patch Changes diff --git a/plugins/api-docs/api-report-alpha.md b/plugins/api-docs/api-report-alpha.md index 1d22283884..6003056cb3 100644 --- a/plugins/api-docs/api-report-alpha.md +++ b/plugins/api-docs/api-report-alpha.md @@ -13,8 +13,9 @@ const _default: BackstagePlugin< root: RouteRef; }, { - registerApi: ExternalRouteRef; - } + registerApi: ExternalRouteRef; + }, + {} >; export default _default; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4f88e8874a..96b52002d8 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.8-next.1", + "version": "0.11.8-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", @@ -63,7 +63,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@graphiql/react": "^0.20.0", + "@graphiql/react": "^0.23.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index c0a59fca0e..305d90e2ec 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-app-backend +## 0.3.72-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.72-next.1 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 7ca6b7924d..d6ac29953e 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.72-next.1", + "version": "0.3.72-next.2", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", @@ -82,7 +82,7 @@ "@backstage/cli": "workspace:^", "@backstage/types": "workspace:^", "@types/supertest": "^2.0.8", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "supertest": "^6.1.3" }, "configSchema": "config.d.ts" diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index efd406fc95..25861bc8a1 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-node +## 0.1.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + ## 0.1.23-next.1 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index 3d02ac0388..e5e4d19a47 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.23-next.1", + "version": "0.1.23-next.2", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index 0a676a49cf..a7f8d8a012 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-app-visualizer +## 0.1.9-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/app-visualizer/api-report.md b/plugins/app-visualizer/api-report.md index 764caa7190..fd3749b159 100644 --- a/plugins/app-visualizer/api-report.md +++ b/plugins/app-visualizer/api-report.md @@ -6,7 +6,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @public (undocumented) -const visualizerPlugin: BackstagePlugin<{}, {}>; +const visualizerPlugin: BackstagePlugin<{}, {}, {}>; export default visualizerPlugin; // (No @packageDocumentation comment for this package) diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index 784848a573..0d825be137 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 2356f38cfa..b2c9685bab 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -189,18 +189,12 @@ function OutputLink(props: { }) { const routeRef = props.node?.instance?.getData(coreExtensionData.routeRef); - let link: string | undefined = undefined; - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - link = useRouteRef(routeRef as RouteRef)(); - } catch { - /* ignore */ - } + const link = useRouteRef(routeRef as RouteRef); return ( {props.dataRef.id}}> - {link ? link : null} + {link ? link : null} ); diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 74b7d191c4..e2014d6749 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index c6416f1cfc..995b980124 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index bf64404405..ce99594759 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index c7a03d7177..2ddace0546 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", @@ -44,7 +44,7 @@ "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", "node-cache": "^5.1.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index cfc576d6e1..471f86842e 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 521318cb37..f48e7ce40b 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index 87da903953..e70bd7f8ee 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 912f0c99a7..ae9692bfe8 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index f1423273d7..493b57d26a 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.1.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index e33a50c25d..c388ead3cb 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", @@ -40,7 +40,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 6d000cede5..3b4f24a64a 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.18-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 098416f347..20b3f88606 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.2.18-next.1", + "version": "0.2.18-next.2", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index cb68d5ab5c..83db20aaa5 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index 13504d3a2f..e773518782 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index 4bf32bad63..8f502140b7 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index c77669aaac..88fd39a53b 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index 08739b52cf..64d59dc5aa 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 209a422980..ed2e536396 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index 94904ce214..4c5df385c7 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index 9ddc9f5877..d41612d32a 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 4e640b0b73..9bbf02ba3c 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.1.18-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.18-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index eab709c3a3..f18104ba15 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.1.18-next.1", + "version": "0.1.18-next.2", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", @@ -38,7 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "passport-microsoft": "^1.0.0" }, "devDependencies": { diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 066695c188..63c6662332 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index b475437611..77a00a52d6 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index acad1b96a8..a9d6523201 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index ff6d31ebee..4df4e14dfe 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.1.16-next.1", + "version": "0.1.16-next.2", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index b0686455b5..6c32155ebe 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index 90fd9c4b63..5f59a4e0d1 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index 12266f8aba..38fe3bed5d 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.0.16-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index a2460cc5f3..ad0c5c5ecb 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.0.16-next.1", + "version": "0.0.16-next.2", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index b2ab669eca..476df484d2 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index c299519e45..1d1feee009 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index 3dcce4d28f..8b23915d26 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index a33c321292..c720d9b501 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index cc50745204..9f81a53961 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index 8fcf5fb2a4..166b7e8e82 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index d17fdabb26..f99c8d0453 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-auth-backend +## 0.22.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.22.10-next.1 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 086e5de7c5..8dec959418 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.22.10-next.1", + "version": "0.22.10-next.2", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", @@ -87,7 +87,7 @@ "minimatch": "^9.0.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "openid-client": "^5.2.1", "passport": "^0.7.0", "passport-auth0": "^1.4.3", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index e8b2a4f4b9..1905329e0a 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-node +## 0.5.0-next.2 + +### Minor Changes + +- 579afd0: **BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.18-next.1 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 3ce4490790..348db40dd7 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.4.18-next.1", + "version": "0.5.0-next.2", "backstage": { "role": "node-library", "pluginId": "auth", @@ -49,7 +49,7 @@ "express": "^4.17.1", "jose": "^5.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "passport": "^0.7.0", "winston": "^3.2.1", "zod": "^3.22.4", diff --git a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts index a3387b7038..fccfcb94de 100644 --- a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts +++ b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts @@ -43,11 +43,10 @@ export function createOAuthProviderFactory(options: { return ctx => { return OAuthEnvironmentHandler.mapConfig(ctx.config, envConfig => { const signInResolver = - options.signInResolver ?? readDeclarativeSignInResolver({ config: envConfig, signInResolverFactories: options.signInResolverFactories ?? {}, - }); + }) ?? options.signInResolver; return createOAuthRouteHandlers({ authenticator: options.authenticator, diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index c4ce7148f4..30e2e77a6e 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.22-next.1 + +### Patch Changes + +- 3fca643: Added method `listBranchesByRepository` to `BitbucketCloudClient` +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + ## 0.2.22-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 34c245facc..833a367c03 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -12,6 +12,12 @@ export class BitbucketCloudClient { config: BitbucketCloudIntegrationConfig, ): BitbucketCloudClient; // (undocumented) + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination; + // (undocumented) listProjectsByWorkspace( workspace: string, options?: FilterAndSortOptions & PartialResponseOptions, @@ -209,6 +215,9 @@ export namespace Models { size?: number; values?: Array | Set; } + export interface PaginatedBranches extends Paginated { + values?: Set; + } export interface PaginatedProjects extends Paginated { values?: Set; } diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index 06fd4e6f3a..d2f22cc966 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.2.22-next.0", + "version": "0.2.22-next.1", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts index f625f0daf8..7558c92138 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts @@ -162,4 +162,33 @@ describe('BitbucketCloudClient', () => { expect(results).toHaveLength(1); expect(results[0].slug).toEqual('workspace1'); }); + + it('listBranchesByRepository', async () => { + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace1/repo1/refs/branches', + (_, res, ctx) => { + const response = { + values: [ + { + type: 'branch', + name: 'branch1', + } as Models.Branch, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + const pagination = client.listBranchesByRepository('repo1', 'workspace1'); + + const results = []; + for await (const result of pagination.iterateResults()) { + results.push(result); + } + + expect(results).toHaveLength(1); + expect(results[0].name).toEqual('branch1'); + }); }); diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts index f1b7d60e41..86f56dd70d 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts @@ -95,6 +95,26 @@ export class BitbucketCloudClient { ); } + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + const workspaceEnc = encodeURIComponent(workspace); + + return new WithPagination( + paginationOptions => + this.createUrl( + `/repositories/${workspaceEnc}/${repository}/refs/branches`, + { + ...paginationOptions, + ...options, + }, + ), + url => this.getTypeMapped(url), + ); + } + private createUrl(endpoint: string, options?: RequestOptions): URL { const request = new URL(this.config.apiBaseUrl + endpoint); for (const key in options) { diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 2cdc143d06..28b1b45e1b 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -275,6 +275,17 @@ export namespace Models { values?: Set; } + /** + * A paginated list of branches. + * @public + */ + export interface PaginatedBranches extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + /** * Object describing a user's role on resources like commits or pull requests. * @public diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 705712aac6..69c16e9875 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.3.18-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 96c57eb821..443dfebb87 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.3.18-next.1", + "version": "0.3.18-next.2", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index d7b4c90862..89d6847511 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.43-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + ## 0.1.43-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 02f4094131..de18ae5895 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.1.43-next.1", + "version": "0.1.43-next.2", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", @@ -58,7 +58,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 17b20064ba..771702d86d 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index d854bbd8e5..d9ba70c806 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.2.6-next.1", + "version": "0.2.6-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 58c54afce9..ec27de045c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.2.10-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 03ae26f4c2..3f4389788c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.2.10-next.1", + "version": "0.2.10-next.2", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 762d73c1b6..46469b747a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.37-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.37-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 7f35ea702c..eeafae91d5 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.37-next.1", + "version": "0.1.37-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", @@ -59,7 +59,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@types/node-fetch": "^2.5.12", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 0a5ed326fd..f5f805edd9 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.1.24-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 9abc3da395..0673dd1805 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.1.24-next.1", + "version": "0.1.24-next.2", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index 78f6172a3d..d4b173cc3f 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.40-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.40-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index abec87fab7..b10e48d34b 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.40-next.1", + "version": "0.1.40-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", @@ -55,7 +55,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index f0f37b1c03..ebaa50af30 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.1.18-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 562b20401a..e1f60aec67 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.1.18-next.1", + "version": "0.1.18-next.2", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index e42812d75d..c7a032c43e 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-github +## 0.6.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.6.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index ca2042f214..dfde8e9718 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.6.6-next.1", + "version": "0.6.6-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", @@ -67,7 +67,7 @@ "git-url-parse": "^14.0.0", "lodash": "^4.17.21", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index 33ce83336c..5611260f87 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.0.6-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 79c43d6529..68e1c8f252 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.0.6-next.1", + "version": "0.0.6-next.2", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 2b8cffa0d6..737f79de59 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.3.22-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.3.22-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 8cc2b6ac3a..d15deafd08 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.3.22-next.1", + "version": "0.3.22-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", @@ -62,7 +62,7 @@ "@backstage/plugin-events-node": "workspace:^", "@gitbeaker/rest": "^40.0.3", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index d43ff7902d..e56e14b95f 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.28-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 8d3d25188d..f3744d9b07 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.4.28-next.1", + "version": "0.4.28-next.2", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index a93f095557..8b8907bd7b 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index efda8a6b40..fa627cb55f 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.7.1-next.1", + "version": "0.7.1-next.2", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md index a62e7d7984..4637f19052 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.0.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json index 2cf01d9e2c..a6609c7227 100644 --- a/plugins/catalog-backend-module-logs/package.json +++ b/plugins/catalog-backend-module-logs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-logs", - "version": "0.0.2-next.1", + "version": "0.0.2-next.2", "description": "A module that subscribes to catalog releated events and logs them.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 86bf7e2e45..2efe171ec1 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.5.31-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index fc0502201b..daba6c4b9f 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.5.31-next.1", + "version": "0.5.31-next.2", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", @@ -62,7 +62,7 @@ "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.0.2", "qs": "^6.9.4", "uuid": "^9.0.0" diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 8daf794314..eb0dbcf08d 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.41-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index c6147804f9..aa4ec5a803 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.1.41-next.1", + "version": "0.1.41-next.2", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 924e0a9df0..209160b4e1 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index ad76a864d3..977379fefa 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.1.29-next.1", + "version": "0.1.29-next.2", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", @@ -63,7 +63,7 @@ "@backstage/types": "workspace:^", "lodash": "^4.17.21", "luxon": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 262105e62a..0813e4b6ab 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-model@1.5.0 + ## 0.1.21-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 0b3b1a9534..02c8a09ae9 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.1.21-next.1", + "version": "0.1.21-next.2", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index 435637c3d8..37413e5f02 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.4.10-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index a1078106eb..eaf86d75ed 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.4.10-next.1", + "version": "0.4.10-next.2", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 826f0c5897..fe707b2a9a 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-catalog-backend +## 1.24.1-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.24.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index c78d5dc578..0a046b43e4 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "1.24.1-next.1", + "version": "1.24.1-next.2", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", @@ -90,7 +90,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.0.2", "prom-client": "^15.0.0", "uuid": "^9.0.0", diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 615bcc538e..2d0fde55da 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-common +## 1.0.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/catalog-model@1.5.0 + ## 1.0.26-next.0 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 167c1b0a91..cfcfee74d8 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-common", - "version": "1.0.26-next.0", + "version": "1.0.26-next.1", "description": "Common functionalities for the catalog plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 5648055a3e..695af8d4fe 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-graph +## 0.4.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + ## 0.4.8-next.2 ### Patch Changes diff --git a/plugins/catalog-graph/api-report-alpha.md b/plugins/catalog-graph/api-report-alpha.md index 3895f12fcf..e651946463 100644 --- a/plugins/catalog-graph/api-report-alpha.md +++ b/plugins/catalog-graph/api-report-alpha.md @@ -13,15 +13,13 @@ const _default: BackstagePlugin< catalogGraph: RouteRef; }, { - catalogEntity: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; - } + catalogEntity: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, + {} >; export default _default; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 04fe835f5c..08a5497dc0 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.8-next.2", + "version": "0.4.8-next.3", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 43e132823a..2063f9e2f9 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-import +## 0.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + ## 0.12.2-next.1 ### Patch Changes diff --git a/plugins/catalog-import/api-report-alpha.md b/plugins/catalog-import/api-report-alpha.md index 63e2cda339..51e3d99fd7 100644 --- a/plugins/catalog-import/api-report-alpha.md +++ b/plugins/catalog-import/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { importPage: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 397d3c92d9..44076a3fbc 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.12.2-next.1", + "version": "0.12.2-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index f276e0a93e..24f1446c6d 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-node +## 1.12.5-next.2 + +### Patch Changes + +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.12.5-next.1 ### Patch Changes diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index e14f55c196..ba02079ceb 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -96,7 +96,7 @@ export interface CatalogProcessingExtensionPoint { export const catalogProcessingExtensionPoint: ExtensionPoint; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 3e755330e3..330427c7b5 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.12.5-next.1", + "version": "1.12.5-next.2", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 38c15e8957..24a1cf6572 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-react +## 1.12.3-next.2 + +### Patch Changes + +- 012e3eb: Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.12.3-next.1 ### Patch Changes diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index 206c0160e2..edfa183903 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -115,7 +115,15 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export function createEntityContentExtension< @@ -148,7 +156,12 @@ export function createEntityContentExtension< filter?: string | undefined; title?: string | undefined; path?: string | undefined; - } + }, + never, + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index fdbd942529..7300729db5 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.12.3-next.1", + "version": "1.12.3-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index e81b8e5ebf..42d69726b6 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -83,7 +83,7 @@ export function createEntityCardExtension< id: 'entity-content:catalog/overview', input: 'cards', }, - disabled: options.disabled ?? true, + disabled: options.disabled, output: { element: coreExtensionData.reactElement, filterFunction: catalogExtensionData.entityFilterFunction.optional(), @@ -137,7 +137,7 @@ export function createEntityContentExtension< id: 'page:catalog/entity', input: 'contents', }, - disabled: options.disabled ?? true, + disabled: options.disabled, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath, diff --git a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md index e3dc87146f..b299631a21 100644 --- a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-unprocessed-entities-common +## 0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + ## 0.0.4-next.0 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities-common/package.json b/plugins/catalog-unprocessed-entities-common/package.json index 8bded795b4..e5f88ebba4 100644 --- a/plugins/catalog-unprocessed-entities-common/package.json +++ b/plugins/catalog-unprocessed-entities-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities-common", - "version": "0.0.4-next.0", + "version": "0.0.4-next.1", "description": "Common functionalities for the catalog-unprocessed-entities plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 0a229ff17a..48daaa7774 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog +## 1.22.0-next.2 + +### Minor Changes + +- 6925dcb: Introduces the HasSubdomainsCard component that displays the subdomains of a given domain + +### Patch Changes + +- 604a504: The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + ## 1.21.2-next.1 ### Patch Changes diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index a62b047c4c..5019fc25ea 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -85,6 +85,8 @@ export const catalogTranslationRef: TranslationRef< readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system'; readonly 'hasSubcomponentsCard.title': 'Has subcomponents'; readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component'; + readonly 'hasSubdomainsCard.title': 'Has subdomains'; + readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain'; readonly 'hasSystemsCard.title': 'Has systems'; readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain'; readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.'; @@ -106,7 +108,15 @@ export function createCatalogFilterExtension< inputs?: TInputs; configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) const _default: BackstagePlugin< @@ -119,24 +129,19 @@ const _default: BackstagePlugin< }>; }, { - viewTechDoc: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; - createComponent: ExternalRouteRef; - createFromTemplate: ExternalRouteRef< - { - namespace: string; - templateName: string; - }, - true - >; - unregisterRedirect: ExternalRouteRef; - } + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + createComponent: ExternalRouteRef; + createFromTemplate: ExternalRouteRef<{ + namespace: string; + templateName: string; + }>; + unregisterRedirect: ExternalRouteRef; + }, + {} >; export default _default; diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index c1fb52fb94..95333a6451 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -370,6 +370,11 @@ export const EntityHasSubcomponentsCard: ( props: HasSubcomponentsCardProps, ) => JSX.Element; +// @public (undocumented) +export const EntityHasSubdomainsCard: ( + props: HasSubdomainsCardProps, +) => JSX.Element; + // @public (undocumented) export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element; @@ -551,6 +556,16 @@ export interface HasSubcomponentsCardProps { variant?: InfoCardVariants; } +// @public (undocumented) +export interface HasSubdomainsCardProps { + // (undocumented) + tableOptions?: TableOptions; + // (undocumented) + title?: string; + // (undocumented) + variant?: InfoCardVariants; +} + // @public (undocumented) export interface HasSystemsCardProps { // (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e747170032..0208271d1a 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.21.2-next.1", + "version": "1.22.0-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index da6a6a669e..8a18efabde 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -46,6 +46,7 @@ export const catalogLabelsEntityCard = createEntityCardExtension({ export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ name: 'depends-on-components', + filter: 'kind:component', loader: async () => import('../components/DependsOnComponentsCard').then(m => compatWrapper(), @@ -54,6 +55,7 @@ export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ name: 'depends-on-resources', + filter: 'kind:component', loader: async () => import('../components/DependsOnResourcesCard').then(m => compatWrapper(), @@ -62,6 +64,7 @@ export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ export const catalogHasComponentsEntityCard = createEntityCardExtension({ name: 'has-components', + filter: 'kind:system', loader: async () => import('../components/HasComponentsCard').then(m => compatWrapper(), @@ -70,6 +73,7 @@ export const catalogHasComponentsEntityCard = createEntityCardExtension({ export const catalogHasResourcesEntityCard = createEntityCardExtension({ name: 'has-resources', + filter: 'kind:system', loader: async () => import('../components/HasResourcesCard').then(m => compatWrapper(), @@ -78,14 +82,25 @@ export const catalogHasResourcesEntityCard = createEntityCardExtension({ export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ name: 'has-subcomponents', + filter: 'kind:component', loader: async () => import('../components/HasSubcomponentsCard').then(m => compatWrapper(), ), }); +export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ + name: 'has-subdomains', + filter: 'kind:domain', + loader: async () => + import('../components/HasSubdomainsCard').then(m => + compatWrapper(), + ), +}); + export const catalogHasSystemsEntityCard = createEntityCardExtension({ name: 'has-systems', + filter: 'kind:domain', loader: async () => import('../components/HasSystemsCard').then(m => compatWrapper(), @@ -101,5 +116,6 @@ export default [ catalogHasComponentsEntityCard, catalogHasResourcesEntityCard, catalogHasSubcomponentsEntityCard, + catalogHasSubdomainsEntityCard, catalogHasSystemsEntityCard, ]; diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx new file mode 100644 index 0000000000..6c145b3228 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright 2020 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 { Entity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { waitFor, screen } from '@testing-library/react'; +import React from 'react'; +import { HasSubdomainsCard } from './HasSubdomainsCard'; + +describe('', () => { + const getEntitiesByRefs: jest.MockedFunction< + CatalogApi['getEntitiesByRefs'] + > = jest.fn(); + let Wrapper: React.ComponentType>; + + beforeEach(() => { + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [], + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('Has subdomains')).toBeInTheDocument(); + expect( + screen.getByText(/No subdomain is part of this domain/i), + ).toBeInTheDocument(); + }); + + it('shows related subdomains', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [ + { + targetRef: 'domain:my-namespace/target-name', + type: RELATION_HAS_PART, + }, + ], + }; + getEntitiesByRefs.mockResolvedValue({ + items: [ + { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }, + ], + }); + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(screen.getByText('Has subdomains')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx new file mode 100644 index 0000000000..811f74d820 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2020 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 { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants, TableOptions } from '@backstage/core-components'; +import React from 'react'; +import { + asComponentEntities, + componentEntityColumns, + RelatedEntitiesCard, +} from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @public */ +export interface HasSubdomainsCardProps { + variant?: InfoCardVariants; + tableOptions?: TableOptions; + title?: string; +} + +export function HasSubdomainsCard(props: HasSubdomainsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); + const { + variant = 'gridItem', + tableOptions = {}, + title = t('hasSubdomainsCard.title'), + } = props; + return ( + + ); +} diff --git a/plugins/catalog/src/components/HasSubdomainsCard/index.ts b/plugins/catalog/src/components/HasSubdomainsCard/index.ts new file mode 100644 index 0000000000..a740abbcc2 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 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 { HasSubdomainsCard } from './HasSubdomainsCard'; +export type { HasSubdomainsCardProps } from './HasSubdomainsCard'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index a691011c96..806ea40c66 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -49,6 +49,7 @@ export { EntityHasComponentsCard, EntityHasResourcesCard, EntityHasSubcomponentsCard, + EntityHasSubdomainsCard, EntityHasSystemsCard, EntityLinksCard, EntityLabelsCard, @@ -71,6 +72,7 @@ export type { EntityContextMenuClassKey } from './components/EntityContextMenu'; export type { HasComponentsCardProps } from './components/HasComponentsCard'; export type { HasResourcesCardProps } from './components/HasResourcesCard'; export type { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +export type { HasSubdomainsCardProps } from './components/HasSubdomainsCard'; export type { HasSystemsCardProps } from './components/HasSystemsCard'; export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 33ceaef632..a339ff31a8 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -50,6 +50,7 @@ import { DependsOnResourcesCardProps } from './components/DependsOnResourcesCard import { HasComponentsCardProps } from './components/HasComponentsCard'; import { HasResourcesCardProps } from './components/HasResourcesCard'; import { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +import { HasSubdomainsCardProps } from './components/HasSubdomainsCard'; import { HasSystemsCardProps } from './components/HasSystemsCard'; import { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; import { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; @@ -199,6 +200,19 @@ export const EntityHasSubcomponentsCard: ( }), ); +/** @public */ +export const EntityHasSubdomainsCard: ( + props: HasSubdomainsCardProps, +) => JSX.Element = catalogPlugin.provide( + createComponentExtension({ + name: 'EntityHasSubdomainsCard', + component: { + lazy: () => + import('./components/HasSubdomainsCard').then(m => m.HasSubdomainsCard), + }, + }), +); + /** @public */ export const EntityHasResourcesCard: ( props: HasResourcesCardProps, diff --git a/plugins/catalog/src/translation.ts b/plugins/catalog/src/translation.ts index 2699b58542..f38a34be84 100644 --- a/plugins/catalog/src/translation.ts +++ b/plugins/catalog/src/translation.ts @@ -143,6 +143,10 @@ export const catalogTranslationRef = createTranslationRef({ title: 'Has subcomponents', emptyMessage: 'No subcomponent is part of this component', }, + hasSubdomainsCard: { + title: 'Has subdomains', + emptyMessage: 'No subdomain is part of this domain', + }, hasSystemsCard: { title: 'Has systems', emptyMessage: 'No system is part of this domain', diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 323385dc82..db4d04fc6e 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-devtools-backend +## 0.3.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index f2f39dea9a..64cfc4f883 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.3.9-next.1", + "version": "0.3.9-next.2", "backstage": { "role": "backend-plugin", "pluginId": "devtools", @@ -56,7 +56,7 @@ "express-promise-router": "^4.1.0", "fs-extra": "^11.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "ping": "^0.4.1", "semver": "^7.5.3", "yn": "^4.0.0" diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md index 09e8a85edb..80d0584c77 100644 --- a/plugins/devtools-common/CHANGELOG.md +++ b/plugins/devtools-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-devtools-common +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + ## 0.1.12-next.0 ### Patch Changes diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json index be1fab2967..d8a87753ca 100644 --- a/plugins/devtools-common/package.json +++ b/plugins/devtools-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-common", - "version": "0.1.12-next.0", + "version": "0.1.12-next.1", "description": "Common functionalities for the devtools plugin", "backstage": { "role": "common-library", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index 496ff5cf52..afebfeefe2 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-devtools +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/devtools/api-report-alpha.md b/plugins/devtools/api-report-alpha.md index 59aefd3698..23ede8144c 100644 --- a/plugins/devtools/api-report-alpha.md +++ b/plugins/devtools/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index a6956161c6..264f7af95b 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 53a4e8a3fa..a4a4da2b17 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.3.9-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 0a4ef481ff..7b1da53130 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.3.9-next.1", + "version": "0.3.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 0c13627d71..804293b869 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index c24b8743ac..5d7fc3f1bf 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 0c333e1d8f..edef56d946 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 87fb26f2e5..a58bacdf8c 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 2cd77d9933..fcdda38e9b 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 43fd0446eb..91e598511f 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 068b54c0b0..38b1e277e8 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-github +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index 57516356ea..f77c2c9df8 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index b717e5886d..a7986891fa 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index 34643977cb..c5a0abfeb8 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 33ad7e0318..f408883694 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.2 + ## 0.1.33-next.1 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index a20f4fa7c7..0cff95bf35 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.33-next.1", + "version": "0.1.33-next.2", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 1bbf2359e4..8c2e2f767b 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-events-backend +## 0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + ## 0.3.10-next.1 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index e563551d5b..01310be5d8 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.3.10-next.1", + "version": "0.3.10-next.2", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index a01292b23e..ec7c864d23 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-node +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + ## 0.3.9-next.1 ### Patch Changes diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 5b45b4419e..729f526dee 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -66,11 +66,12 @@ export type EventsServiceEventHandler = (params: EventParams) => Promise; export const eventsServiceFactory: ServiceFactoryCompat< EventsService, 'plugin', + 'singleton', undefined >; // @public -export const eventsServiceRef: ServiceRef; +export const eventsServiceRef: ServiceRef; // @public (undocumented) export type EventsServiceSubscribeOptions = { diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 7943aadc8f..db2a6f4271 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.3.9-next.1", + "version": "0.3.9-next.2", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index cff91b2e5c..370053d097 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @internal/plugin-todo-list-backend +## 1.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + ## 1.0.30-next.1 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 2e84c6c2b7..b9b04c178f 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.30-next.1", + "version": "1.0.30-next.2", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index f780b14906..b2f2175573 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @internal/plugin-todo-list-common +## 1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + ## 1.0.21-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 6071032963..cca7f5aa4b 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.21-next.0", + "version": "1.0.21-next.1", "backstage": { "role": "common-library", "pluginId": "todo-list", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 2cd0a184fd..08cf9bec5f 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-home +## 0.7.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + ## 0.7.9-next.1 ### Patch Changes diff --git a/plugins/home/api-report-alpha.md b/plugins/home/api-report-alpha.md index eb693a8e6f..0f9b980a78 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin<{}, {}>; +const _default: BackstagePlugin<{}, {}, {}>; export default _default; // @alpha (undocumented) diff --git a/plugins/home/package.json b/plugins/home/package.json index 792f7a1525..e566b46e78 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.7.9-next.1", + "version": "0.7.9-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index c286d49164..1255d6d0fd 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-kubernetes-backend +## 0.18.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 8c1aa06: Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. + This was already documented and supported by the plugin. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.18.4-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index 777a3392db..d6c5f1d32b 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -68,6 +68,11 @@ export interface Config { caData?: string; /** @visibility secret */ caFile?: string; + customResources?: Array<{ + group: string; + apiVersion: string; + plural: string; + }>; }>; } | { diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 71154e0712..70f640e0ba 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.18.4-next.1", + "version": "0.18.4-next.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", @@ -92,7 +92,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "morgan": "^1.10.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index cab3826e64..801c2018c9 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + ## 0.0.14-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index f2b1feb906..75c0758ac5 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.14-next.1", + "version": "0.0.14-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 9ff6547ad4..f8ac4b4768 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-common +## 0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.8.2-next.0 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 7ac04abea5..1e05ed873e 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-common", - "version": "0.8.2-next.0", + "version": "0.8.2-next.1", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index 6e738ab3aa..42e5885c49 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes-node +## 0.1.17-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 7597206bd0..5377f26b4d 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", @@ -43,7 +43,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^", "@kubernetes/client-node": "^0.20.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 8859563287..34e9b259be 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-react +## 0.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.2-next.1 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 094fdaa5e2..dbf89ae0cf 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.4.2-next.1", + "version": "0.4.2-next.2", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 384f6574bc..1b5ca07506 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-kubernetes +## 0.11.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + ## 0.11.13-next.1 ### Patch Changes diff --git a/plugins/kubernetes/api-report-alpha.md b/plugins/kubernetes/api-report-alpha.md index 8bd6e98baa..befbcf379a 100644 --- a/plugins/kubernetes/api-report-alpha.md +++ b/plugins/kubernetes/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { kubernetes: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index d711c5bdb5..ab8fe84d9b 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.11.13-next.1", + "version": "0.11.13-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 1a9e5ad4bc..a0b4bc8caa 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-notifications-backend-module-email +## 0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.2.0-next.1 ### Minor Changes diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index 7bbac05b62..1defad74b4 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -2,7 +2,7 @@ Adds support for sending Backstage notifications as emails to users. -Supports sending emails using `SMTP`, `SES`, or `sendmail`. +Supports sending emails using `SMTP`, `SES`, `sendmail`, or `stream` (for debugging purposes). ## Customizing email content diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 92f3385d1b..04e6f7910f 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -79,6 +79,10 @@ export interface Config { * Newline style, defaults to 'unix' */ newline?: 'unix' | 'windows'; + } + | { + /** Only for debugging, disables the actual sending of emails */ + transport: 'stream'; }; /** * Sender email address @@ -132,6 +136,14 @@ export interface Config { */ excludedTopics?: string[]; }; + /** + * White list of addresses to send email to + */ + allowlistEmailAddresses?: string[]; + /** + * Black list of addresses to not send email to + */ + denylistEmailAddresses?: string[]; }; }; }; diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 425186fcad..33451341ea 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.2.0-next.1", + "version": "0.2.0-next.2", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index 3bfccfcc90..03388a77a2 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -37,6 +37,7 @@ import { createSendmailTransport, createSesTransport, createSmtpTransport, + createStreamTransport, } from './transports'; import { UserEntity } from '@backstage/catalog-model'; import { compact } from 'lodash'; @@ -56,6 +57,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private readonly throttleInterval: number; private readonly frontendBaseUrl: string; private readonly filter: NotificationProcessorFilters; + private readonly allowlistEmailAddresses?: string[]; + private readonly denylistEmailAddresses?: string[]; constructor( private readonly logger: LoggerService, @@ -85,6 +88,12 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ? durationToMilliseconds(readDurationFromConfig(cacheConfig)) : 3_600_000; this.frontendBaseUrl = config.getString('app.baseUrl'); + this.allowlistEmailAddresses = emailProcessorConfig.getOptionalStringArray( + 'allowlistEmailAddresses', + ); + this.denylistEmailAddresses = emailProcessorConfig.getOptionalStringArray( + 'denylistEmailAddresses', + ); this.filter = getProcessorFiltersFromConfig(emailProcessorConfig); } @@ -105,6 +114,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ); } else if (transport === 'sendmail') { this.transporter = createSendmailTransport(this.transportConfig); + } else if (transport === 'stream') { + this.transporter = createStreamTransport(); } else { throw new Error(`Unsupported transport: ${transport}`); } @@ -197,14 +208,30 @@ export class NotificationsEmailProcessor implements NotificationProcessor { notification: Notification, options: NotificationSendOptions, ) { + let emails: string[]; if (options.recipients.type === 'broadcast' || notification.user === null) { - return await this.getBroadcastEmails(); + emails = await this.getBroadcastEmails(); + } else { + emails = await this.getUserEmail(notification.user); } - return await this.getUserEmail(notification.user); + + if (this.allowlistEmailAddresses) { + emails = emails.filter(email => + this.allowlistEmailAddresses?.includes(email), + ); + } + + if (this.denylistEmailAddresses) { + emails = emails.filter( + email => !this.denylistEmailAddresses?.includes(email), + ); + } + return emails; } private async sendMail(options: Mail.Options) { try { + this.logger.debug(`Sending notification email to ${options.to}`); await this.transporter.sendMail(options); } catch (e) { this.logger.error(`Failed to send email to ${options.to}: ${e}`); diff --git a/plugins/notifications-backend-module-email/src/processor/transports/index.ts b/plugins/notifications-backend-module-email/src/processor/transports/index.ts index ccea9f77cc..9a679f3723 100644 --- a/plugins/notifications-backend-module-email/src/processor/transports/index.ts +++ b/plugins/notifications-backend-module-email/src/processor/transports/index.ts @@ -16,3 +16,4 @@ export { createSmtpTransport } from './smtp'; export { createSesTransport } from './ses'; export { createSendmailTransport } from './sendmail'; +export { createStreamTransport } from './stream'; diff --git a/plugins/notifications-backend-module-email/src/processor/transports/stream.ts b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts new file mode 100644 index 0000000000..26ed401366 --- /dev/null +++ b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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 { createTransport } from 'nodemailer'; + +export const createStreamTransport = () => { + return createTransport({ streamTransport: true }); +}; diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 9afeff1a51..61cba9e3a6 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-notifications-backend +## 0.3.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.3.4-next.1 ### Patch Changes diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md index 60c9a4e078..a6b6615d9f 100644 --- a/plugins/notifications-backend/README.md +++ b/plugins/notifications-backend/README.md @@ -4,13 +4,7 @@ Welcome to the notifications backend plugin! ## Getting started -Add the notifications to your backend: - -```ts -const backend = createBackend(); -// ... -backend.add(import('@backstage/plugin-notifications-backend')); -``` +To install, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. For users to be able to see notifications in real-time, you have to install also the signals plugin (`@backstage/plugin-signals-node`, `@backstage/plugin-signals-backend`, and @@ -18,58 +12,8 @@ the signals plugin (`@backstage/plugin-signals-node`, `@backstage/plugin-signals ## Extending Notifications -The notifications can be extended with `NotificationProcessor`. These processors allow to decorate notifications -before they are sent or/and send the notifications to external services. - -Start off by creating a notification processor: - -```ts -import { Notification } from '@backstage/plugin-notifications-common'; -import { NotificationProcessor } from '@backstage/plugin-notifications-node'; - -class MyNotificationProcessor implements NotificationProcessor { - async decorate(notification: Notification): Promise { - if (notification.origin === 'plugin-my-plugin') { - notification.payload.icon = 'my-icon'; - } - return notification; - } - - async send(notification: Notification): Promise { - nodemailer.sendEmail({ - from: 'backstage', - to: 'user', - subject: notification.payload.title, - text: notification.payload.description, - }); - } -} -``` - -Both of the processing functions are optional, and you can implement only one of them. - -Add the notification processor to the notification system by: - -```ts -import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; -import { Notification } from '@backstage/plugin-notifications-common'; - -export const myPlugin = createBackendPlugin({ - pluginId: 'myPlugin', - register(env) { - env.registerInit({ - deps: { - notifications: notificationsProcessingExtensionPoint, - // ... - }, - async init({ notifications }) { - // ... - notifications.addProcessor(new MyNotificationProcessor()); - }, - }); - }, -}); -``` +When a notification is created, it's processing can be customized via `processors`. +Please refer [Backstage documentation](https://backstage.io/docs/notifications) for further details. ## Sending Notifications By Backend Plugins diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 5916b18116..9bc26f7a1f 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.3.4-next.1", + "version": "0.3.4-next.2", "backstage": { "role": "backend-plugin", "pluginId": "notifications", @@ -52,7 +52,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts new file mode 100644 index 0000000000..b06d6f203b --- /dev/null +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2024 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 { mockServices } from '@backstage/backend-test-utils'; +import { getUsersForEntityRef } from './getUsersForEntityRef'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, +} from '@backstage/catalog-model'; + +describe('getUsersForEntityRef', () => { + const catalogApiMock = { + getEntitiesByRefs: jest.fn(), + getEntityByRef: jest.fn(), + }; + const authMock = mockServices.auth(); + + it('should return empty array if entityRef is null', async () => { + await expect( + getUsersForEntityRef(null, [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual([]); + }); + + it('should resolve users without calling catalog', async () => { + await expect( + getUsersForEntityRef(['user:foo', 'user:ignored'], ['user:ignored'], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:foo']); + expect(catalogApiMock.getEntitiesByRefs).not.toHaveBeenCalled(); + }); + + it('should resolve group entities to users', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'parent_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + { + type: RELATION_PARENT_OF, + targetRef: 'group:default/child_group', + }, + ], + }, + ], + }); + + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'child_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/bar', + }, + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/ignored', + }, + ], + }, + ], + }); + + await expect( + getUsersForEntityRef( + 'group:default/parent_group', + ['user:default/ignored'], + { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }, + ), + ).resolves.toEqual(['user:default/foo', 'user:default/bar']); + }); + + it('should resolve user owner of entity from entity ref', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'user:default/foo', + }, + ], + }, + ], + }); + + await expect( + getUsersForEntityRef('component:default/test_component', [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:default/foo']); + }); + + it('should resolve group owner of entity from entity ref', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'group:default/owner_group', + }, + ], + }, + ], + }); + + catalogApiMock.getEntityByRef.mockResolvedValueOnce({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'owner_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + ], + }); + + await expect( + getUsersForEntityRef('component:default/test_component', [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:default/foo']); + }); +}); diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts new file mode 100644 index 0000000000..fd564d6528 --- /dev/null +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2024 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 { + Entity, + isGroupEntity, + isUserEntity, + parseEntityRef, + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; + +const isUserEntityRef = (ref: string) => + parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; + +// Partitions array of entity references to two arrays; user entity refs and other entity refs +const partitionEntityRefs = (refs: string[]): string[][] => + refs.reduce( + ([userEntityRefs, otherEntityRefs]: string[][], ref: string) => { + return isUserEntityRef(ref) + ? [[...userEntityRefs, ref], otherEntityRefs] + : [userEntityRefs, [...otherEntityRefs, ref]]; + }, + [[], []], + ); + +export const getUsersForEntityRef = async ( + entityRef: string | string[] | null, + excludeEntityRefs: string | string[], + options: { + auth: AuthService; + catalogClient: CatalogApi; + }, +): Promise => { + const { auth, catalogClient } = options; + + if (entityRef === null) { + return []; + } + + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + + const excluded = Array.isArray(excludeEntityRefs) + ? excludeEntityRefs + : [excludeEntityRefs]; + + const refsArr = Array.isArray(entityRef) ? entityRef : [entityRef]; + const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(refsArr); + const users: string[] = userEntityRefs.filter(ref => !excluded.includes(ref)); + const entityRefs = otherEntityRefs.filter(ref => !excluded.includes(ref)); + + const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; + let entities: Array = []; + if (entityRefs.length > 0) { + const fetchedEntities = await catalogClient.getEntitiesByRefs( + { + entityRefs, + fields, + }, + { token }, + ); + entities = fetchedEntities.items; + } + + const mapEntity = async (entity: Entity | undefined): Promise => { + if (!entity) { + return []; + } + + const currentEntityRef = stringifyEntityRef(entity); + if (excluded.includes(currentEntityRef)) { + return []; + } + + if (isUserEntity(entity)) { + return [currentEntityRef]; + } + + if (isGroupEntity(entity)) { + if (!entity.relations?.length) { + return []; + } + + const groupUsers = entity.relations + .filter( + relation => + relation.type === RELATION_HAS_MEMBER && + isUserEntityRef(relation.targetRef), + ) + .map(r => r.targetRef); + + const childGroupRefs = entity.relations + .filter(relation => relation.type === RELATION_PARENT_OF) + .map(r => r.targetRef); + + let childGroupUsers: string[][] = []; + if (childGroupRefs.length > 0) { + const childGroups = await catalogClient.getEntitiesByRefs( + { + entityRefs: childGroupRefs, + fields, + }, + { token }, + ); + childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); + } + + return [...groupUsers, ...childGroupUsers.flat(2)].filter( + ref => !excluded.includes(ref), + ); + } + + if (entity.relations?.length) { + const ownerRef = entity.relations.find( + relation => relation.type === RELATION_OWNED_BY, + )?.targetRef; + + if (!ownerRef) { + return []; + } + + if (isUserEntityRef(ownerRef)) { + if (excluded.includes(ownerRef)) { + return []; + } + return [ownerRef]; + } + + const owner = await catalogClient.getEntityByRef(ownerRef, { token }); + return mapEntity(owner); + } + + return []; + }; + + for (const entity of entities) { + const u = await mapEntity(entity); + users.push(...u); + } + + return [...new Set(users)]; +}; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 3ca3ce46bc..abd7e2dc13 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -23,15 +23,6 @@ import { } from '../database'; import { v4 as uuid } from 'uuid'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { - Entity, - isGroupEntity, - isUserEntity, - RELATION_HAS_MEMBER, - RELATION_OWNED_BY, - RELATION_PARENT_OF, - stringifyEntityRef, -} from '@backstage/catalog-model'; import { NotificationProcessor, NotificationSendOptions, @@ -54,6 +45,7 @@ import { NotificationStatus, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; +import { getUsersForEntityRef } from './getUsersForEntityRef'; /** @internal */ export interface RouterOptions { @@ -94,91 +86,6 @@ export async function createRouter( return info.userEntityRef; }; - const getUsersForEntityRef = async ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], - ): Promise => { - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: await auth.getOwnServiceCredentials(), - targetPluginId: 'catalog', - }); - - if (entityRef === null) { - return []; - } - - const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; - - const refs = Array.isArray(entityRef) ? entityRef : [entityRef]; - const entities = await catalogClient.getEntitiesByRefs( - { - entityRefs: refs, - fields, - }, - { token }, - ); - - const excluded = Array.isArray(excludeEntityRefs) - ? excludeEntityRefs - : [excludeEntityRefs]; - - const mapEntity = async (entity: Entity | undefined): Promise => { - if (!entity) { - return []; - } - - const currentEntityRef = stringifyEntityRef(entity); - if (excluded.includes(currentEntityRef)) { - return []; - } - - if (isUserEntity(entity)) { - return [currentEntityRef]; - } else if (isGroupEntity(entity) && entity.relations) { - const users = entity.relations - .filter(relation => relation.type === RELATION_HAS_MEMBER) - .map(r => r.targetRef); - - const childGroupRefs = entity.relations - .filter(relation => relation.type === RELATION_PARENT_OF) - .map(r => r.targetRef); - - const childGroups = await catalogClient.getEntitiesByRefs( - { - entityRefs: childGroupRefs, - fields, - }, - { token }, - ); - const childGroupUsers = await Promise.all( - childGroups.items.map(mapEntity), - ); - return [...users, ...childGroupUsers.flat(2)]; - } else if (entity.relations) { - const ownerRef = entity.relations.find( - relation => relation.type === RELATION_OWNED_BY, - )?.targetRef; - - if (ownerRef) { - const owner = await catalogClient.getEntityByRef(ownerRef, { token }); - if (owner) { - return mapEntity(owner); - } - } - } - - return []; - }; - - const users: string[] = []; - for (const entity of entities.items) { - const u = await mapEntity(entity); - users.push(...u); - } - - return users; - }; - const filterProcessors = (payload: NotificationPayload) => { const result: NotificationProcessor[] = []; @@ -543,6 +450,7 @@ export async function createRouter( users = await getUsersForEntityRef( entityRef, recipients.excludeEntityRef ?? [], + { auth, catalogClient }, ); } catch (e) { logger.error(`Failed to resolve notification receivers: ${e}`); diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 6907885ce7..627cb53ea0 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-notifications-node +## 0.2.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 37f28fec35..995b98bc21 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -65,7 +65,11 @@ export interface NotificationService { } // @public (undocumented) -export const notificationService: ServiceRef; +export const notificationService: ServiceRef< + NotificationService, + 'plugin', + 'singleton' +>; // @public (undocumented) export type NotificationServiceOptions = { diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index e3b61e2734..533ebe7ce2 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", @@ -44,7 +44,7 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "knex": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md index 63212d2c16..78bcf4e753 100644 --- a/plugins/notifications/README.md +++ b/plugins/notifications/README.md @@ -2,38 +2,11 @@ Welcome to the notifications plugin! -_This plugin was created through the Backstage CLI_ - ## Getting started -First, install the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages. -See the documentation for installation instructions. +To install, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. -To add the notifications main menu, add the following to your `packages/app/src/components/Root/Root.tsx`: - -```tsx -import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; - - - - - // ... - - - -; -``` - -Also add the route to notifications to `packages/app/src/App.tsx`: - -```tsx -import { NotificationsPage } from '@backstage/plugin-notifications'; - - - // ... - } /> -; -``` +Please mind installing the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages before this frontend plugin. ## Real-time notifications diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 9240bdb1c2..5e06ae2616 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -176,20 +176,5 @@ export function useNotificationsApi( value: T; }; -// @public (undocumented) -export function useTitleCounter(): { - setNotificationCount: (newCount: number) => void; -}; - -// @public (undocumented) -export function useWebNotifications(enabled: boolean): { - sendWebNotification: (options: { - id: string; - title: string; - description: string; - link?: string; - }) => Notification | null; -}; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 7b55c63747..457893b016 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -114,7 +114,9 @@ export const NotificationsSidebarItem = (props?: { const notificationsRoute = useRouteRef(rootRouteRef); // TODO: Do we want to add long polling in case signals are not available const { lastSignal } = useSignal('notifications'); - const { sendWebNotification } = useWebNotifications(webNotificationsEnabled); + const { sendWebNotification, requestUserPermission } = useWebNotifications( + webNotificationsEnabled, + ); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -126,6 +128,19 @@ export const NotificationsSidebarItem = (props?: { component={Link} to={notification.payload.link ?? notificationsRoute()} onClick={() => { + if (notification.payload.link) { + notificationsApi + .updateNotifications({ + ids: [notification.id], + read: true, + }) + .catch(() => { + alertApi.post({ + message: 'Failed to mark notification as read', + severity: 'error', + }); + }); + } closeSnackbar(snackBarId); }} > @@ -259,6 +274,9 @@ export const NotificationsSidebarItem = (props?: { )} { + requestUserPermission(); + }} hasNotifications={!error && !!unreadCount} text={text} icon={icon} diff --git a/plugins/notifications/src/hooks/useTitleCounter.ts b/plugins/notifications/src/hooks/useTitleCounter.ts index 0793cd58d6..399bfb84f4 100644 --- a/plugins/notifications/src/hooks/useTitleCounter.ts +++ b/plugins/notifications/src/hooks/useTitleCounter.ts @@ -25,7 +25,7 @@ const throttledSetTitle = throttle((shownTitle: string) => { document.title = shownTitle; }, 100); -/** @public */ +/** @internal */ export function useTitleCounter() { const [title, setTitle] = useState(document.title); const [count, setCount] = useState(0); diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 728d992130..ec120a614d 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -13,19 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { rootRouteRef } from '../routes'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; +import { notificationsApiRef } from '../api'; -/** @public */ +/** @internal */ export function useWebNotifications(enabled: boolean) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); const notificationsRoute = useRouteRef(rootRouteRef); + const notificationsApi = useApi(notificationsApiRef); const navigate = useNavigate(); - useEffect(() => { + const requestUserPermission = useCallback(() => { if ( enabled && 'Notification' in window && @@ -57,6 +59,10 @@ export function useWebNotifications(enabled: boolean) { event.preventDefault(); if (options.link) { window.open(options.link, '_blank'); + notificationsApi.updateNotifications({ + ids: [options.id], + read: true, + }); } else { navigate(notificationsRoute()); } @@ -65,8 +71,8 @@ export function useWebNotifications(enabled: boolean) { return notification; }, - [webNotificationPermission, navigate, notificationsRoute], + [webNotificationPermission, notificationsApi, navigate, notificationsRoute], ); - return { sendWebNotification }; + return { sendWebNotification, requestUserPermission }; } diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index bb5d76a0cf..25565f1ae1 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -15,5 +15,5 @@ */ export { notificationsPlugin, NotificationsPage } from './plugin'; export * from './api'; -export * from './hooks'; +export { useNotificationsApi } from './hooks'; export * from './components'; diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index e5994b4ec4..4bbf343849 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-org-react +## 0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.27-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 3dca7e029b..d91d73c4ca 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.27-next.1", + "version": "0.1.27-next.2", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 69d6b3fc0f..923d5d1d23 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-org +## 0.6.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.6.28-next.1 ### Patch Changes diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md index 8c5391d857..be74cde141 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -10,8 +10,9 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; const _default: BackstagePlugin< {}, { - catalogIndex: ExternalRouteRef; - } + catalogIndex: ExternalRouteRef; + }, + {} >; export default _default; diff --git a/plugins/org/package.json b/plugins/org/package.json index 72ea35013d..44ae3d10f6 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.28-next.1", + "version": "0.6.28-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 559697105d..7c0765ad43 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + ## 0.1.20-next.1 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index fb40f025ce..0ba16a61f7 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.1.20-next.1", + "version": "0.1.20-next.2", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 9f0f3cf8ac..89955c0f69 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-permission-backend +## 0.5.47-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.5.47-next.1 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 100bde718f..7a331c1ccd 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.47-next.1", + "version": "0.5.47-next.2", "backstage": { "role": "backend-plugin", "pluginId": "permission", @@ -63,7 +63,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yn": "^4.0.0", "zod": "^3.22.4" }, diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index 9194c9cae6..8ef1eb314f 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-permission-common +## 0.8.1-next.1 + +### Patch Changes + +- df784fe: Add the MetadataResponse type from @backstage/plugin-permission-node, since this + type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.8.1-next.0 ### Patch Changes diff --git a/plugins/permission-common/api-report.md b/plugins/permission-common/api-report.md index 12dd49d090..465c43846f 100644 --- a/plugins/permission-common/api-report.md +++ b/plugins/permission-common/api-report.md @@ -126,6 +126,12 @@ export function isResourcePermission( // @public export function isUpdatePermission(permission: Permission): boolean; +// @public +export type MetadataResponse = { + permissions?: Permission[]; + rules: MetadataResponseSerializedRule[]; +}; + // @public export type MetadataResponseSerializedRule = { name: string; diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 323dea5638..faeaf4a972 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-common", - "version": "0.8.1-next.0", + "version": "0.8.1-next.1", "description": "Isomorphic types and client for Backstage permissions and authorization", "backstage": { "role": "common-library", diff --git a/plugins/permission-common/src/types/index.ts b/plugins/permission-common/src/types/index.ts index 1d2243447f..f5380541df 100644 --- a/plugins/permission-common/src/types/index.ts +++ b/plugins/permission-common/src/types/index.ts @@ -40,7 +40,10 @@ export type { NotCriteria, } from './api'; export type { DiscoveryApi } from './discovery'; -export type { MetadataResponseSerializedRule } from './integration'; +export type { + MetadataResponse, + MetadataResponseSerializedRule, +} from './integration'; export type { BasicPermission, PermissionAttributes, diff --git a/plugins/permission-common/src/types/integration.ts b/plugins/permission-common/src/types/integration.ts index 6c208bb775..d531140923 100644 --- a/plugins/permission-common/src/types/integration.ts +++ b/plugins/permission-common/src/types/integration.ts @@ -15,6 +15,7 @@ */ import zodToJsonSchema from 'zod-to-json-schema'; +import { Permission } from './permission'; /** * Serialized permission rules, with the paramsSchema @@ -28,3 +29,14 @@ export type MetadataResponseSerializedRule = { resourceType: string; paramsSchema?: ReturnType; }; + +/** + * Response type for the .metadata endpoint in + * {@link @backstage/plugin-permission-node#createPermissionIntegrationRouter} + * + * @public + */ +export type MetadataResponse = { + permissions?: Permission[]; + rules: MetadataResponseSerializedRule[]; +}; diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 3e29fe0c2a..282ea5fda8 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-permission-node +## 0.8.1-next.2 + +### Patch Changes + +- df784fe: The MetadataResponse type has been moved to @backstage/plugin-permission-common + to match the recent move of MetadataResponseSerializedRule, and should be + imported from there going forward. To avoid an immediate breaking change, this + type is still re-exported from this package, but is marked as deprecated and + will be removed in a future release. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.8.1-next.1 ### Patch Changes diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 8a90a39728..36483bd342 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -17,6 +17,7 @@ import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentifiedPermissionMessage } from '@backstage/plugin-permission-common'; +import { MetadataResponse as MetadataResponse_2 } from '@backstage/plugin-permission-common'; import { MetadataResponseSerializedRule as MetadataResponseSerializedRule_2 } from '@backstage/plugin-permission-common'; import { NotCriteria } from '@backstage/plugin-permission-common'; import { Permission } from '@backstage/plugin-permission-common'; @@ -188,11 +189,8 @@ export const makeCreatePermissionRule: < rule: PermissionRule, ) => PermissionRule; -// @public -export type MetadataResponse = { - permissions?: Permission[]; - rules: MetadataResponseSerializedRule[]; -}; +// @public @deprecated +export type MetadataResponse = MetadataResponse_2; // @public @deprecated export type MetadataResponseSerializedRule = MetadataResponseSerializedRule_2; diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 4e024a3f10..b2c2d06f11 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.8.1-next.1", + "version": "0.8.1-next.2", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 7793d8ab8e..40d215d515 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -24,6 +24,7 @@ import { AuthorizeResult, DefinitivePolicyDecision, IdentifiedPermissionMessage, + MetadataResponse as CommonMetadataResponse, MetadataResponseSerializedRule as CommonMetadataResponseSerializedRule, Permission, PermissionCondition, @@ -119,11 +120,9 @@ export type MetadataResponseSerializedRule = * Response type for the .metadata endpoint. * * @public + * @deprecated Please import from `@backstage/plugin-permission-common` instead. */ -export type MetadataResponse = { - permissions?: Permission[]; - rules: MetadataResponseSerializedRule[]; -}; +export type MetadataResponse = CommonMetadataResponse; const applyConditions = ( criteria: PermissionCriteria>, diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 8b2e8e3250..83f4dea7ed 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-permission-react +## 0.4.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.4.25-next.0 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 8bd566a34c..394f56ff02 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.25-next.0", + "version": "0.4.25-next.1", "backstage": { "role": "web-library", "pluginId": "permission", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index bfec70e942..138b98902b 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-proxy-backend +## 0.5.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.5.4-next.1 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index ad4627b907..091e637886 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.5.4-next.1", + "version": "0.5.4-next.2", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", @@ -79,7 +79,7 @@ "@types/uuid": "^9.0.0", "@types/yup": "^0.32.0", "msw": "^2.0.0", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "configSchema": "config.d.ts" } diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index fe925ffb7d..90541db310 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index 19470655e6..92d64ec7f0 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts index 35868274fd..dda65a76ea 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts @@ -19,7 +19,10 @@ import { ConfigReader } from '@backstage/config'; import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; import { WebApi } from 'azure-devops-node-api'; -import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { + getRepoSourceDirectory, + initRepoAndPush, +} from '@backstage/plugin-scaffolder-node'; import { examples } from './azure.examples'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -48,6 +51,10 @@ describe('publish:azure examples', () => { host: 'dev.azure.com', credentials: [{ personalAccessToken: 'tokenlols' }], }, + { + host: 'test.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, ], }, }); @@ -114,4 +121,172 @@ describe('publish:azure examples', () => { gitAuthorInfo: {}, }); }); + + it(`should ${examples[3].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: input.gitCommitMessage, + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[4].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { + name: input.gitAuthorName, + email: input.gitAuthorEmail, + }, + }); + }); + + it(`should ${examples[5].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath), + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[6].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: input.token }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[7].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://test.azure.com/organization/project/_git/repo', + webUrl: 'https://test.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://test.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts index 22c2299b30..ae5bfece2b 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts @@ -70,4 +70,94 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Initializes an Azure DevOps repository with a custom commit message', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + gitCommitMessage: 'Initial setup and configuration', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository with a custom author name and email', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + gitAuthorName: 'John Doe', + gitAuthorEmail: 'john.doe@example.com', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using a specific source path', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + sourcePath: 'path/to/source', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using an authentication token', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + token: 'personal-access-token', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using an custom repo url', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'test.azure.com?organization=organization&project=project&repo=repo', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 0d52996a88..604b44d3da 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 3fca643: Added autocompletion support for resource `branches` +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index f5bbd2df63..1806189dd4 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -49,7 +49,7 @@ "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts index 0447ff2d28..d99a8358b9 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -35,6 +35,11 @@ describe('handleAutocompleteRequest', () => { .fn() .mockReturnValue([{ values: [{ slug: 'repository1' }] }]), }), + listBranchesByRepository: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ name: 'branch1' }] }]), + }), }; const fromConfig = jest @@ -89,6 +94,19 @@ describe('handleAutocompleteRequest', () => { expect(result).toEqual({ results: [{ title: 'repository1' }] }); }); + it('should return branches', async () => { + const result = await handleAutocompleteRequest({ + token: 'foo', + resource: 'branches', + context: { + workspace: 'workspace1', + repository: 'repository1', + }, + }); + + expect(result).toEqual({ results: [{ title: 'branch1' }] }); + }); + it('should throw an error when passing an invalid resource', async () => { await expect( handleAutocompleteRequest({ diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts index 31a2103088..75cc170a30 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -34,29 +34,29 @@ export async function handleAutocompleteRequest({ switch (resource) { case 'workspaces': { - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client.listWorkspaces().iteratePages()) { - const slugs = [...page.values!].map(p => p.slug!); - result.push(...slugs); + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'projects': { if (!context.workspace) throw new InputError('Missing workspace context parameter'); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listProjectsByWorkspace(context.workspace) .iteratePages()) { - const keys = [...page.values!].map(p => p.key!); - result.push(...keys); + const keys = [...page.values!].map(p => ({ title: p.key! })); + results.push(...keys); } - return { results: result.map(title => ({ title })) }; + return { results }; } case 'repositories': { if (!context.workspace || !context.project) @@ -64,18 +64,35 @@ export async function handleAutocompleteRequest({ 'Missing workspace and/or project context parameter', ); - const result: string[] = []; + const results: { title: string }[] = []; for await (const page of client .listRepositoriesByWorkspace(context.workspace, { q: `project.key="${context.project}"`, }) .iteratePages()) { - const slugs = [...page.values!].map(p => p.slug!); - result.push(...slugs); + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); } - return { results: result.map(title => ({ title })) }; + return { results }; + } + case 'branches': { + if (!context.workspace || !context.repository) + throw new InputError( + 'Missing workspace and/or repository context parameter', + ); + + const results: { title: string }[] = []; + + for await (const page of client + .listBranchesByRepository(context.repository, context.workspace) + .iteratePages()) { + const names = [...page.values!].map(p => ({ title: p.name! })); + results.push(...names); + } + + return { results }; } default: throw new InputError(`Invalid resource: ${resource}`); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 7d4b793d10..221a626169 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index c4c00f96e7..5c08e23b6f 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -48,7 +48,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index 8ab0a80df0..062d54226d 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.2.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index 584f61b502..a9f499ec74 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.2.13-next.1", + "version": "0.2.13-next.2", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -50,7 +50,7 @@ "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 9c9b493c1d..2621b1d8f3 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.24-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.24-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 5b54e3841d..b4b10f7d4c 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.2.24-next.1", + "version": "0.2.24-next.2", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -53,7 +53,7 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", "git-url-parse": "^14.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-html-markdown": "^1.3.0", "yaml": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 71ce038f43..160953da23 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.47-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index ec1899099b..7749830304 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.2.47-next.1", + "version": "0.2.47-next.2", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index 551141df6c..c759549d90 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json index 1c2feb106a..33459ba5b6 100644 --- a/plugins/scaffolder-backend-module-gcp/package.json +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gcp", - "version": "0.1.1-next.1", + "version": "0.1.1-next.2", "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 6827109801..811e9a80cc 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 68bdd36f44..0e50124013 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -47,7 +47,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 983d3cebb0..0a05340f77 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 432daf6c8b..cb888b65ad 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -47,7 +47,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index d39143c7c4..e9d841aa0c 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index 68a0475622..c373839d13 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.4.1-next.1", + "version": "0.4.1-next.2", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts index ee7b5caa65..f0aa0ea5a2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts @@ -52,9 +52,14 @@ describe('github:pages', () => { let githubCredentialsProvider: GithubCredentialsProvider; let action: TemplateAction; - const input = yaml.parse(examples[0].example).steps[0].input; const mockContext = createMockActionContext({ - input, + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/', + token: 'gph_YourGitHubToken', + }, }); beforeEach(() => { @@ -68,8 +73,20 @@ describe('github:pages', () => { afterEach(jest.resetAllMocks); - it('should work with example input', async () => { - await action.handler(mockContext); + it(`Should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); expect(mockOctokit.request).toHaveBeenCalledWith( 'POST /repos/{owner}/{repo}/pages', @@ -87,4 +104,343 @@ describe('github:pages', () => { }, ); }); + it(`Should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'customPathRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'legacyOwner', + repo: 'legacyRepo', + build_type: 'legacy', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'branchOwner', + repo: 'customBranchRepo', + build_type: 'workflow', + source: { + branch: 'develop', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'fullCustomRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'minimalOwner', + repo: 'minimalRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'customBuildPathRepo', + build_type: 'legacy', + source: { + branch: 'main', + path: '/custom-path', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'branchPathOwner', + repo: 'customBranchPathRepo', + build_type: 'workflow', + source: { + branch: 'feature-branch', + path: '/project-docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwnerName', + repo: 'customRepoName', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'tokenOwner', + repo: 'customTokenRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/site', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[10].description}`, async () => { + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'tokenOwner', + repo: 'specificTokenRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[11].description}`, async () => { + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'docsOwner', + repo: 'docSiteRepo', + build_type: 'workflow', + source: { + branch: 'docs-branch', + path: '/docs-site', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts index be4a33e5df..57102c4b92 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts @@ -37,4 +37,209 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Enables GitHub Pages for a repository with a custom source path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-path', + name: 'Enable GitHub Pages with Custom Source Path', + input: { + repoUrl: 'github.com?repo=customPathRepo&owner=customOwner', + sourcePath: '/docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository using legacy build type.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-legacy', + name: 'Enable GitHub Pages with Legacy Build Type', + input: { + repoUrl: 'github.com?repo=legacyRepo&owner=legacyOwner', + buildType: 'legacy', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom source branch.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-branch', + name: 'Enable GitHub Pages with Custom Source Branch', + input: { + repoUrl: 'github.com?repo=customBranchRepo&owner=branchOwner', + sourceBranch: 'develop', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + + { + description: + 'Enables GitHub Pages for a repository with full customization.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-full-custom', + name: 'Enable GitHub Pages with Full Customization', + input: { + repoUrl: 'github.com?repo=fullCustomRepo&owner=customOwner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with minimal configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-minimal', + name: 'Enable GitHub Pages with Minimal Configuration', + input: { + repoUrl: 'github.com?repo=minimalRepo&owner=minimalOwner', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with custom build type and source path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-build-path', + name: 'Enable GitHub Pages with Custom Build Type and Source Path', + input: { + repoUrl: 'github.com?repo=customBuildPathRepo&owner=customOwner', + buildType: 'legacy', + sourcePath: '/custom-path', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with custom source branch and path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-branch-path', + name: 'Enable GitHub Pages with Custom Source Branch and Path', + input: { + repoUrl: + 'github.com?repo=customBranchPathRepo&owner=branchPathOwner', + sourceBranch: 'feature-branch', + sourcePath: '/project-docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom owner and repository name.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-owner-repo', + name: 'Enable GitHub Pages with Custom Owner and Repository Name', + input: { + repoUrl: 'github.com?repo=customRepoName&owner=customOwnerName', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with full customization and a different token.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-full-custom-diff-token', + name: 'Enable GitHub Pages with Full Customization and Different Token', + input: { + repoUrl: 'github.com?repo=customTokenRepo&owner=tokenOwner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/site', + token: 'gph_DifferentGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a specific token for authorization.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-specific-token', + name: 'Enable GitHub Pages with Specific Token', + input: { + repoUrl: 'github.com?repo=specificTokenRepo&owner=tokenOwner', + token: 'gph_SpecificGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a documentation site with custom configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-doc-site', + name: 'Enable GitHub Pages for Documentation Site', + input: { + repoUrl: 'github.com?repo=docSiteRepo&owner=docsOwner', + buildType: 'workflow', + sourceBranch: 'docs-branch', + sourcePath: '/docs-site', + token: 'gph_DocsGitHubToken', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts index 843a32f758..1a81a54c64 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts @@ -58,6 +58,7 @@ const mockOctokit = { getRepoPublicKey: jest.fn(), }, }, + request: jest.fn().mockResolvedValue({}), }; jest.mock('octokit', () => ({ Octokit: class { @@ -188,4 +189,2218 @@ describe('github:repo:create examples', () => { has_wiki: false, // disable wiki }); }); + + it(`Should ${examples[3].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://example.com', + }); + }); + + it(`Should ${examples[4].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[5].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[6].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[7].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: false, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[8].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'pull_request_title', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[9].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'blank', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[10].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: true, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[11].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[12].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[12].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[13].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[13].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[14].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[14].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[15].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[15].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[16].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[16].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[17].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[17].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: true, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[18].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[18].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[19].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[19].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[20].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[20].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: 'A repository for project XYZ', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://project-xyz.com', + }); + }); + + it(`Should ${examples[21].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[21].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[22].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[22].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[23].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[23].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: true, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[24].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[24].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[25].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[25].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: false, + description: undefined, + has_projects: false, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[26].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[26].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[27].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[27].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[28].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[28].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for project ABC', + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[29].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[29].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'public', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[30].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[30].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[31].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[31].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[32].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[32].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[33].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[33].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[34].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[34].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: 'Internal repository for team collaboration', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://internal.example.com', + }); + }); + + it(`Should ${examples[35].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[35].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[36].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[36].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: true, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[37].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[37].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[38].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[38].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[39].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[39].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[40].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[40].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for web application project', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://webapp.example.com', + }); + }); + + it(`Should ${examples[41].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[41].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'pull_request_description', + allow_merge_commit: false, + allow_rebase_merge: false, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[42].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[42].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: false, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[43].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[43].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[44].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[44].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[45].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[45].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'public', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[46].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[46].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: false, + description: 'Repository for backend service', + has_projects: undefined, + has_wiki: false, + homepage: undefined, + }); + }); + + it(`Should ${examples[47].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[47].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[48].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[48].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[49].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[49].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[50].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[50].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: true, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[51].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[51].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://example.com', + }); + }); + + it(`Should ${examples[52].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[52].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for microservice development', + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[53].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[53].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[54].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[54].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: true, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[55].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[55].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[56].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[56].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts index a58da1ee29..1c1f1ca5d2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts @@ -63,4 +63,932 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Set repository homepage.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with homepage', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + homepage: 'https://example.com', + }, + }, + ], + }), + }, + { + description: 'Create a private repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + }, + }, + ], + }), + }, + { + description: 'Enable required code owner reviews.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required code owner reviews', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + }, + }, + ], + }), + }, + { + description: 'Set required approving review count to 2.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required approving review count', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: 'Allow squash merge only.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only squash merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowSquashMerge: true, + allowRebaseMerge: false, + }, + }, + ], + }), + }, + { + description: 'Set squash merge commit title to pull request title.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with squash merge commit title set to pull request title', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + squashMergeCommitTitle: 'pull_request_title', + }, + }, + ], + }), + }, + { + description: 'Set squash merge commit message to blank.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with squash merge commit message set to blank', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + squashMergeCommitMessage: 'blank', + }, + }, + ], + }), + }, + { + description: 'Allow auto-merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing auto-merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowAutoMerge: true, + }, + }, + ], + }), + }, + { + description: 'Set collaborators with push access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with collaborators having push access', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + collaborators: [ + { username: 'user1', permission: 'push' }, + { username: 'user2', permission: 'push' }, + ], + }, + }, + ], + }), + }, + { + description: 'Add topics to repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['devops', 'kubernetes', 'ci-cd'], + }, + }, + ], + }), + }, + { + description: 'Add secret variables to repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'SECRET_KEY', value: 'supersecretkey' }, + { name: 'API_TOKEN', value: 'tokenvalue' }, + ], + }, + }, + ], + }), + }, + { + description: 'Enable branch protection requiring status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/circleci: build'], + }, + }, + ], + }), + }, + { + description: 'Require branches to be up-to-date before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring branches to be up-to-date before merging', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: 'Require conversation resolution before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring conversation resolution before merging', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + }, + }, + ], + }), + }, + { + description: 'Delete branch on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch deletion on merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + deleteBranchOnMerge: true, + }, + }, + ], + }), + }, + { + description: 'Customize OIDC token.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC token customization', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://github.com', + }, + }, + }, + ], + }), + }, + { + description: 'Require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Set multiple properties including description, homepage, and visibility.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple properties', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'A repository for project XYZ', + homepage: 'https://project-xyz.com', + repoVisibility: 'internal', + }, + }, + ], + }), + }, + { + description: 'Configure branch protection with multiple settings.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection settings', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: [ + 'ci/circleci: build', + 'ci/circleci: test', + ], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: true, + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: + 'Set repository access to private and add collaborators with admin access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository with collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + collaborators: [ + { username: 'admin1', permission: 'admin' }, + { username: 'admin2', permission: 'admin' }, + ], + }, + }, + ], + }), + }, + { + description: 'Enable GitHub Projects for the repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with GitHub Projects enabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + hasProjects: true, + }, + }, + ], + }), + }, + { + description: + 'Disable merge commits and allow only rebase and squash merges.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only rebase and squash merges', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowRebaseMerge: true, + allowSquashMerge: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository access to internal with no projects and issues.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository without projects and issues', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + hasProjects: false, + hasIssues: false, + }, + }, + ], + }), + }, + { + description: + 'Create repository with OIDC customization for specific audience.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization for specific audience', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://specific-audience.com', + }, + }, + }, + ], + }), + }, + { + description: 'Require all branches to be up-to-date before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring all branches to be up-to-date', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: 'Set description and topics for the repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for project ABC', + topics: ['python', 'machine-learning', 'data-science'], + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to public and enable commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new public GitHub repository with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'public', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with collaborators and default branch protection.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with collaborators and branch protection', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + collaborators: [ + { username: 'contributor1', permission: 'write' }, + { username: 'contributor2', permission: 'write' }, + ], + requiredStatusCheckContexts: ['ci/travis: build'], + }, + }, + ], + }), + }, + { + description: 'Add multiple secret variables.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'SECRET_KEY_1', value: 'value1' }, + { name: 'SECRET_KEY_2', value: 'value2' }, + ], + }, + }, + ], + }), + }, + { + description: 'Require a minimum of 2 approving reviews for merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with 2 required approving reviews', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection with conversation resolution required.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection and conversation resolution required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to internal with description and homepage.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository with description and homepage', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + description: 'Internal repository for team collaboration', + homepage: 'https://internal.example.com', + }, + }, + ], + }), + }, + { + description: 'Disable auto-merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with auto-merge disabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowAutoMerge: false, + }, + }, + ], + }), + }, + { + description: 'Set repository topics and enable GitHub Projects.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with topics and GitHub Projects enabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['opensource', 'nodejs', 'api'], + hasProjects: true, + }, + }, + ], + }), + }, + { + description: + 'Create a private repository with collaborators having admin and write access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository with multiple collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + collaborators: [ + { username: 'admin1', permission: 'admin' }, + { username: 'writer1', permission: 'write' }, + ], + }, + }, + ], + }), + }, + { + description: 'Disable branch deletion on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch deletion on merge disabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + deleteBranchOnMerge: false, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to internal and enable commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create repository with description, homepage, and required status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description, homepage, and status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for web application project', + homepage: 'https://webapp.example.com', + requiredStatusCheckContexts: [ + 'ci/travis: build', + 'ci/travis: lint', + ], + }, + }, + ], + }), + }, + { + description: + 'Enable squash merges only and set commit message to pull request description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only squash merges with commit message set to pull request description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowSquashMerge: true, + allowRebaseMerge: false, + squashMergeCommitMessage: 'pull_request_description', + }, + }, + ], + }), + }, + { + description: 'Enable rebase merges only and require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only rebase merges with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowRebaseMerge: true, + allowSquashMerge: false, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create repository with OIDC customization for multiple audiences.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization for multiple audiences', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: ['https://audience1.com', 'https://audience2.com'], + }, + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection with required approving reviews and status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring approving reviews and status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + requiredStatusCheckContexts: [ + 'ci/circleci: build', + 'ci/circleci: test', + ], + }, + }, + ], + }), + }, + { + description: 'Create a public repository with topics and secret variables.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new public GitHub repository with topics and secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'public', + topics: ['javascript', 'react', 'frontend'], + secrets: [ + { name: 'API_KEY', value: 'apikeyvalue' }, + { name: 'DB_PASSWORD', value: 'dbpasswordvalue' }, + ], + }, + }, + ], + }), + }, + { + description: 'Set repository description and disable issues and wiki.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description, and disable issues and wiki', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for backend service', + hasIssues: false, + hasWiki: false, + }, + }, + ], + }), + }, + { + description: 'Enable required conversation resolution and commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required conversation resolution and commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to private and require branches to be up-to-date.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository requiring branches to be up-to-date', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with default settings and add multiple topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with default settings and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['devops', 'ci-cd', 'automation'], + }, + }, + ], + }), + }, + { + description: + 'Disable merge commits, enable auto-merge, and require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository disabling merge commits, enabling auto-merge, and requiring commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowAutoMerge: true, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with homepage, collaborators, and topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with homepage, collaborators, and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + homepage: 'https://example.com', + collaborators: [ + { username: 'user1', permission: 'push' }, + { username: 'user2', permission: 'admin' }, + ], + topics: ['opensource', 'contribution'], + }, + }, + ], + }), + }, + { + description: 'Create a repository with branch protection and description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection and description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/travis: build'], + requiredApprovingReviewCount: 1, + description: 'Repository for microservice development', + }, + }, + ], + }), + }, + { + description: 'Create a repository with OIDC customization and topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://api.example.com', + }, + topics: ['api', 'security'], + }, + }, + ], + }), + }, + { + description: + 'Enable required code owner reviews and branch deletion on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required code owner reviews and branch deletion on merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + deleteBranchOnMerge: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with multiple secret variables and collaborators.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple secret variables and collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'API_SECRET', value: 'secretvalue' }, + { name: 'DB_USER', value: 'dbuser' }, + ], + collaborators: [ + { username: 'dev1', permission: 'write' }, + { username: 'dev2', permission: 'push' }, + ], + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection requiring status checks and conversation resolution.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring status checks and conversation resolution', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/build'], + requiredConversationResolution: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index c0099a16c6..f62e5b4db8 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.5-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 410a6111f5..dde6e48a6e 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.4.5-next.1", + "version": "0.4.5-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts new file mode 100644 index 0000000000..917ba61d91 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts @@ -0,0 +1,711 @@ +/* + * Copyright 2024 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 { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabIssueAction } from './gitlabIssueCreate'; +import { examples } from './gitlabIssueCreate.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Issues: { + create: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issues:create', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(2005, 6, 4, 11, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'sample-token', + apiBaseUrl: 'https://gitlab.com/api/v1', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabIssueAction({ integrations }); + + it(`should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [18], + confidential: false, + epicId: undefined, + labels: '', + createdAt: input.createdAt, + dueDate: input.dueDate, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: input.confidential, + epicId: undefined, + labels: input.labels, + createdAt: input.createdAt, + dueDate: input.dueDate, + discussionToResolve: input.discussionToResolve, + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [18, 22], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[10].description}`, async () => { + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: 'abc123', + mergeRequestToResolveDiscussionsOf: + input.mergeRequestToResolveDiscussionsOf, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts index 760caf0f9f..04fdeba46d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts @@ -50,8 +50,8 @@ export const examples: TemplateExample[] = [ title: 'Test Issue', assignees: [18], description: 'This is the description of the issue', - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', + createdAt: '2022-09-27T18:00:00.000Z', + dueDate: '2022-09-28T12:00:00.000Z', }, }, ], @@ -72,9 +72,9 @@ export const examples: TemplateExample[] = [ assignees: [18, 15], description: 'This is the description of the issue', confidential: false, - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', - discussionToResolve: 1, + createdAt: '2022-09-27T18:00:00.000Z', + dueDate: '2022-09-28T12:00:00.000Z', + discussionToResolve: '1', epicId: 1, labels: 'phase1:label1,phase2:label2', }, @@ -82,4 +82,162 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab issue with token', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue', + description: 'This is the description of the issue', + token: 'sample token', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue with a specific milestone and weight', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue with Milestone', + description: 'This is the description of the issue', + milestoneId: 5, + weight: 3, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type INCIDENT', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'incident', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type TEST', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'test_case', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type TASK with assignees', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'task', + assignees: [18, 22], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type ISSUE and close it', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'issue', + stateEvent: 'close', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type INCIDENT and reopen it', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'incident', + stateEvent: 'reopen', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab issue to resolve a discussion in a merge request', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue for MR Discussion', + description: 'This is the description of the issue', + mergeRequestToResolveDiscussionsOf: 42, + discussionToResolve: 'abc123', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index 31f8197923..85da251d02 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-notifications-common@0.0.5 + ## 0.0.6-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index c4a10f6a92..204bf597e9 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.0.6-next.1", + "version": "0.0.6-next.2", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index ff68b1d78c..ed34f47a58 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.40-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.40-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 38088ccfc8..8cbd5e1067 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.4.40-next.1", + "version": "0.4.40-next.2", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 7515c6494b..c4421d314b 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 382e868: Added test cases for sentry:project:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.31-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 069f310dcc..f683e23d09 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.31-next.1", + "version": "0.1.31-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", @@ -46,7 +46,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.3.3" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts new file mode 100644 index 0000000000..3e31aef3bc --- /dev/null +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2024 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 { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; +import { randomBytes } from 'crypto'; +import { setupServer } from 'msw/node'; +import { HttpResponse, http } from 'msw'; +import { createSentryCreateProjectAction } from './createProject'; +import yaml from 'yaml'; +import { examples } from './createProject.examples'; + +describe('sentry:project:create action', () => { + const worker = setupServer(); + registerMswTestHooks(worker); + + const createScaffolderConfig = (configData: JsonObject = {}) => ({ + config: new ConfigReader({ + scaffolder: { + ...configData, + }, + }), + }); + + const getActionContext = (): ActionContext<{ + organizationSlug: string; + teamSlug: string; + name: string; + slug?: string; + authToken?: string; + }> => + createMockActionContext({ + workspacePath: './dev/proj', + logger: jest.createMockFromModule('winston'), + input: { + organizationSlug: 'org', + teamSlug: 'team', + name: 'test project', + authToken: randomBytes(5).toString('hex'), + }, + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[0].example).steps[1].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer b15711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d4gf93`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project B', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + actionContext.input = { ...actionContext.input, slug: 'project-slug' }; + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer a14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df96`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project A', + slug: 'scaff-proj-a', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const sentryScaffolderConfigToken = randomBytes(5).toString('hex'); + const action = createSentryCreateProjectAction( + createScaffolderConfig({ + sentry: { + token: sentryScaffolderConfigToken, + }, + }), + ); + const actionContext = getActionContext(); + actionContext.input.authToken = undefined; + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer ${sentryScaffolderConfigToken}`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project A', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer c16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf94`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'A very long name for the scaffolded project C that will generate a slug', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts index 9d4551c1b5..3af88a4663 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts @@ -50,4 +50,41 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Creates a Sentry project when no auth token is passed', + example: yaml.stringify({ + steps: [ + { + id: 'create-sentry-project', + action: 'sentry:project:create', + name: 'Create a Sentry project with provided project slug.', + input: { + organizationSlug: 'my-org', + teamSlug: 'team-a', + name: 'Scaffolded project A', + }, + }, + ], + }), + }, + { + description: + 'Creates a Sentry project with a long project name and automatically generated slug.', + example: yaml.stringify({ + steps: [ + { + id: 'create-sentry-project', + action: 'sentry:project:create', + name: 'Create a Sentry project with a long name and no slug provided.', + input: { + organizationSlug: 'my-org', + teamSlug: 'team-c', + name: 'A very long name for the scaffolded project C that will generate a slug', + authToken: + 'c16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf94', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 069390e33f..4833302170 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + - @backstage/types@1.1.1 + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index a820392630..36aed409cc 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.3.7-next.1", + "version": "0.3.7-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b1bb14c6a7..0ed359768e 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-scaffolder-backend +## 1.23.1-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.23.1-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 907a30b541..1e60d5b026 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -427,7 +427,7 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: { createdBy?: string }): Promise<{ + list(options: { createdBy?: string; status?: TaskStatus_2 }): Promise<{ tasks: SerializedTask_2[]; }>; // (undocumented) @@ -646,7 +646,7 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list?(options: { createdBy?: string }): Promise<{ + list?(options: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 8f304690f6..974125ad94 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "1.23.1-next.1", + "version": "1.23.1-next.2", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index f4fae0715b..78a362b5c7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -95,6 +95,33 @@ describe('DatabaseTaskStore', () => { expect(tasks[0].id).toBeDefined(); }); + it('should list filtered created tasks by status', async () => { + const { store } = await createStore(); + + const { taskId } = await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'me', + }); + + await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'him', + }); + + const message = `This task was marked as stale as it exceeded its timeout`; + await store.completeTask({ + taskId, + status: 'cancelled', + eventBody: { message }, + }); + + const { tasks } = await store.list({ status: 'open' }); + expect(tasks.length).toBe(1); + expect(tasks[0].createdBy).toBe('him'); + expect(tasks[0].status).toBe('open'); + expect(tasks[0].id).toBeDefined(); + }); + it('should sent an event to start cancelling the task', async () => { const { store } = await createStore(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index fe62572af9..ba675b2cc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -22,19 +22,19 @@ import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { TaskStore, - TaskStoreEmitOptions, - TaskStoreListEventsOptions, TaskStoreCreateTaskOptions, TaskStoreCreateTaskResult, - TaskStoreShutDownTaskOptions, + TaskStoreEmitOptions, + TaskStoreListEventsOptions, TaskStoreRecoverTaskOptions, + TaskStoreShutDownTaskOptions, } from './types'; import { - SerializedTaskEvent, SerializedTask, - TaskStatus, + SerializedTaskEvent, TaskEventType, TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { DateTime, Duration } from 'luxon'; import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common'; @@ -182,6 +182,7 @@ export class DatabaseTaskStore implements TaskStore { async list(options: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { const queryBuilder = this.db('tasks'); @@ -191,6 +192,10 @@ export class DatabaseTaskStore implements TaskStore { }); } + if (options.status) { + queryBuilder.where({ status: options.status }); + } + const results = await queryBuilder.orderBy('created_at', 'desc').select(); const tasks = results.map(result => ({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index f4946e6652..d17fb33b3b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -20,13 +20,14 @@ import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; import { - TaskSecrets, SerializedTask, SerializedTaskEvent, TaskBroker, TaskBrokerDispatchOptions, TaskCompletionState, TaskContext, + TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { InternalTaskSecrets, TaskStore } from './types'; import { readDuration } from './helper'; @@ -279,13 +280,17 @@ export class StorageTaskBroker implements TaskBroker { async list(options?: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { if (!this.storage.list) { throw new Error( 'TaskStore does not implement the list method. Please implement the list method to be able to list tasks', ); } - return await this.storage.list({ createdBy: options?.createdBy }); + return await this.storage.list({ + createdBy: options?.createdBy, + status: options?.status, + }); } private deferredDispatch = defer(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 0e0ebd706b..89bcce3cc9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { JsonValue, JsonObject, HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { - TemplateAction, - TaskStatus as _TaskStatus, - TaskCompletionState as _TaskCompletionState, SerializedTask as _SerializedTask, - TaskEventType as _TaskEventType, SerializedTaskEvent as _SerializedTaskEvent, - TaskBrokerDispatchResult as _TaskBrokerDispatchResult, - TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, - TaskContext as _TaskContext, TaskBroker as _TaskBroker, + TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, + TaskBrokerDispatchResult as _TaskBrokerDispatchResult, + TaskCompletionState as _TaskCompletionState, + TaskContext as _TaskContext, + TaskEventType as _TaskEventType, + TaskSecrets, + TaskStatus as _TaskStatus, + TemplateAction, } from '@backstage/plugin-scaffolder-node'; /** @@ -190,7 +190,10 @@ export interface TaskStore { tasks: { taskId: string }[]; }>; - list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; emitLogEvent(options: TaskStoreEmitOptions): Promise; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index af06fc4034..01a2fba6e8 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -432,10 +432,11 @@ describe('createRouter', () => { }); const response = await request(app).get( - `/v2/tasks?createdBy=user:default/foo`, + `/v2/tasks?createdBy=user:default/foo&status=completed`, ); expect(taskBroker.list).toHaveBeenCalledWith({ createdBy: 'user:default/foo', + status: 'completed', }); expect(response.status).toEqual(200); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 769a042a78..a25731ce45 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,10 +15,10 @@ */ import { + createLegacyAuthAdapters, HostDiscovery, PluginDatabaseManager, UrlReader, - createLegacyAuthAdapters, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; @@ -35,22 +35,22 @@ import { ScmIntegrations } from '@backstage/integration'; import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, + TemplateEntityStepV1beta3, TemplateEntityV1beta3, templateEntityV1beta3Validator, TemplateParametersV1beta3, - TemplateEntityStepV1beta3, } from '@backstage/plugin-scaffolder-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, scaffolderActionPermissions, + scaffolderTaskPermissions, scaffolderTemplatePermissions, taskCancelPermission, taskCreatePermission, taskReadPermission, templateParameterReadPermission, templateStepReadPermission, - scaffolderTaskPermissions, } from '@backstage/plugin-scaffolder-common/alpha'; import express from 'express'; import Router from 'express-promise-router'; @@ -58,8 +58,9 @@ import { validate } from 'jsonschema'; import { Logger } from 'winston'; import { z } from 'zod'; import { - TemplateAction, TaskBroker, + TaskStatus, + TemplateAction, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; @@ -587,8 +588,17 @@ export async function createRouter( ); } + const [statusQuery] = [req.query.status].flat(); + if ( + typeof statusQuery !== 'string' && + typeof statusQuery !== 'undefined' + ) { + throw new InputError('status query parameter must be a string'); + } + const tasks = await taskBroker.list({ createdBy: userEntityRef, + status: statusQuery ? (statusQuery as TaskStatus) : undefined, }); res.status(200).json(tasks); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 6c2b31d18b..4810656ef4 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-common +## 1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 1.5.5-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index e37ee5462c..949a831906 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.5.5-next.0", + "version": "1.5.5-next.1", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index d49e076730..c7d37fb0c8 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/types@1.1.1 + ## 0.1.10-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index f3ee13fcbe..707ffa1388 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.1.10-next.1", + "version": "0.1.10-next.2", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 3dc6b6b952..42fc13a644 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-node +## 0.4.9-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.9-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 66e8ccd678..515a169cb2 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -315,7 +315,7 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) - list?(options?: { createdBy?: string }): Promise<{ + list?(options?: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 544c40d5b7..6e84b278c2 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.4.9-next.1", + "version": "0.4.9-next.2", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index a2adb97b9c..8324e352f2 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -180,5 +180,8 @@ export interface TaskBroker { get(taskId: string): Promise; - list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options?: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; } diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index ab664aecb3..1661f3d1c7 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-scaffolder-react +## 1.11.0-next.2 + +### Patch Changes + +- 072c00c: Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes +- 04759f2: Fix null check in `isJsonObject` utility function for scaffolder review state component +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.11.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index d4bdd70081..b47bbd731f 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.11.0-next.1", + "version": "1.11.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts index 9b88ff157e..649d0c3160 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2024 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. @@ -33,6 +33,10 @@ describe('isJsonObject', () => { expect(isJsonObject(true)).toBe(false); expect(isJsonObject(undefined)).toBe(false); }); + + it('should return false for null values', () => { + expect(isJsonObject(null)).toBe(false); + }); }); describe('getLastKey', () => { diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts index 49c3b30e7a..d2caf71964 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2024 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. @@ -17,7 +17,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; export function isJsonObject(value?: JsonValue): value is JsonObject { - return typeof value === 'object' && !Array.isArray(value); + return typeof value === 'object' && value !== null && !Array.isArray(value); } // Helper function to get the last part of the key diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx index cbc299bf1c..70f3b1fa59 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -68,6 +68,7 @@ export const DefaultTemplateOutputs = (props: { justifyContent="center" display="flex" gridGap={16} + flexWrap="wrap" > ; }, { - registerComponent: ExternalRouteRef; - viewTechDoc: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; - } + registerComponent: ExternalRouteRef; + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, + {} >; export default _default; diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index d826f6897a..fb53761145 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -384,6 +384,28 @@ export const OwnerPickerFieldSchema: FieldSchema< // @public export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; +// @public +export const RepoBranchPickerFieldExtension: FieldExtensionComponent_2< + string, + { + requestUserCredentials?: + | { + secretsKey: string; + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + gitea?: string[] | undefined; + } + | undefined; + } + | undefined; + } +>; + // @public export const repoPickerValidation: ( value: string, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 5d0b842be6..43b94a9361 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "1.23.1-next.1", + "version": "1.24.0-next.2", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index 31c2870352..459ba682d2 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -173,7 +173,9 @@ export const OngoingTask = (props: { return (
    Run of {templateName} diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..638a05b4ce --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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 { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; + +describe('BitbucketRepoBranchPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest + .fn() + .mockResolvedValue({ results: [{ title: 'branch1' }] }), + }; + + it('renders an input field', () => { + const { getByRole } = render( + + + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + + + , + ); + + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(input, { + target: { value: 'develop' }, + }); + input.blur(); + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); + + it('should populate branches', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = render( + + + , + ); + + // Open the Autcomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the available workspaces are shown + await waitFor(() => expect(getByText('branch1')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('branch1')); + expect(onChange).toHaveBeenCalledWith({ + branch: 'branch1', + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx new file mode 100644 index 0000000000..26853d6d05 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/BitbucketRepoBranchPicker.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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 { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import FormControl from '@material-ui/core/FormControl'; +import React, { useCallback, useState } from 'react'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useApi } from '@backstage/core-plugin-api'; +import { BaseRepoBranchPickerProps } from './types'; +import FormHelperText from '@material-ui/core/FormHelperText'; + +/** + * The underlying component that is rendered in the form for the `BitbucketRepoBranchPicker` + * field extension. + * + * @public + * + */ +export const BitbucketRepoBranchPicker = ({ + onChange, + state, + rawErrors, + accessToken, + required, +}: BaseRepoBranchPickerProps<{ + accessToken?: string; +}>) => { + const { host, workspace, repository, branch } = state; + + const [availableBranches, setAvailableBranches] = useState([]); + + const scaffolderApi = useApi(scaffolderApiRef); + + const updateAvailableBranches = useCallback(() => { + if ( + !scaffolderApi.autocomplete || + !workspace || + !repository || + !accessToken || + host !== 'bitbucket.org' + ) { + setAvailableBranches([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'branches', + context: { workspace, repository }, + provider: 'bitbucket-cloud', + }) + .then(({ results }) => { + setAvailableBranches(results.map(r => r.title)); + }) + .catch(() => { + setAvailableBranches([]); + }); + }, [host, workspace, repository, accessToken, scaffolderApi]); + + useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]); + + return ( + 0 && !branch} + > + { + onChange({ branch: newValue || '' }); + }} + options={availableBranches} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> + The branch of the repository + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..77589e4478 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 { fireEvent, render } from '@testing-library/react'; + +import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; + +describe('DefaultRepoBranchPicker', () => { + it('renders an input field', () => { + const { getByRole } = render( + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + , + ); + + const input = getByRole('textbox'); + + fireEvent.change(input, { + target: { value: 'develop' }, + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx new file mode 100644 index 0000000000..9350e232f8 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/DefaultRepoBranchPicker.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 FormControl from '@material-ui/core/FormControl'; +import React from 'react'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; + +import { BaseRepoBranchPickerProps } from './types'; + +/** + * The underlying component that is rendered in the form for the `DefaultRepoBranchPicker` + * field extension. + * + * @public + * + */ +export const DefaultRepoBranchPicker = ({ + onChange, + state, + rawErrors, + required, +}: BaseRepoBranchPickerProps) => { + const { branch } = state; + + return ( + 0 && !branch} + > + Branch + onChange({ branch: e.target.value })} + value={branch} + /> + The branch of the repository + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx new file mode 100644 index 0000000000..f9c23be407 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.test.tsx @@ -0,0 +1,303 @@ +/* + * Copyright 2024 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 { Form } from '@backstage/plugin-scaffolder-react/alpha'; +import validator from '@rjsf/validator-ajv8'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + scmIntegrationsApiRef, + ScmIntegrationsApi, + scmAuthApiRef, + ScmAuthApi, +} from '@backstage/integration-react'; + +import { + SecretsContextProvider, + scaffolderApiRef, + useTemplateSecrets, + ScaffolderRJSFField, +} from '@backstage/plugin-scaffolder-react'; +import { act, fireEvent } from '@testing-library/react'; +import { RepoBranchPicker } from './RepoBranchPicker'; + +describe('RepoBranchPicker', () => { + const mockIntegrationsApi: Partial = { + byHost: () => ({ type: 'bitbucket' }), + }; + + let mockScmAuthApi: Partial; + + beforeEach(() => { + mockScmAuthApi = { + getCredentials: jest.fn().mockResolvedValue({ token: 'abc123' }), + }; + }); + + describe('happy path rendering', () => { + it('should render the repo branch picker with minimal props', async () => { + const onSubmit = jest.fn(); + + const { getByRole } = await renderInTestApp( + + +
    , + }} + onSubmit={onSubmit} + formContext={{ + formData: {}, + }} + /> + + , + ); + + const input = getByRole('textbox'); + const submitButton = getByRole('button'); + + fireEvent.change(input, { target: { value: 'branch1' } }); + + fireEvent.click(submitButton); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + formData: 'branch1', + }), + expect.anything(), + ); + }); + + it('should render properly with title and description', async () => { + const { getByText } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'bitbucket.org', + }, + }} + /> + + , + ); + + expect(getByText('test title')).toBeInTheDocument(); + expect(getByText('test description')).toBeInTheDocument(); + }); + }); + + describe('requestUserCredentials', () => { + it('should call the scmAuthApi with the correct params', async () => { + const secretsKey = 'testKey'; + + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + const secret = secrets[secretsKey]; + return secret ?
    {secret}
    : null; + }; + + const { getByText } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'github.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://github.com', + additionalScope: { + repoWrite: true, + customScopes: { + github: ['workflow'], + }, + }, + }); + + expect(getByText('abc123')).toBeInTheDocument(); + }); + + it('should call the scmAuthApi with the correct params if workspace is nested', async () => { + await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'gitlab.example.com', + }, + }} + /> + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://gitlab.example.com', + additionalScope: { + repoWrite: true, + }, + }); + }); + + it('should not call the scmAuthApi if secret is available in the state', async () => { + const secretsKey = 'testKey'; + + const SecretsComponent = () => { + const { secrets } = useTemplateSecrets(); + const secret = secrets[secretsKey]; + return secret ?
    {secret}
    : null; + }; + + const { getByText } = await renderInTestApp( + + + , + }} + formContext={{ + formData: { + repoUrl: 'github.com', + }, + }} + /> + + + , + ); + + await act(async () => { + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + // as we already have a secret in the state, getCredentials should not be called again. + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledTimes(0); + + expect(getByText('abc123')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx new file mode 100644 index 0000000000..4ca22c8f80 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -0,0 +1,163 @@ +/* + * Copyright 2024 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 { useApi } from '@backstage/core-plugin-api'; +import { + scmIntegrationsApiRef, + scmAuthApiRef, +} from '@backstage/integration-react'; +import React, { useEffect, useState, useCallback } from 'react'; +import useDebounce from 'react-use/esm/useDebounce'; +import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; + +import { RepoBranchPickerProps } from './schema'; +import { RepoBranchPickerState } from './types'; +import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; +import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; + +/** + * The underlying component that is rendered in the form for the `RepoBranchPicker` + * field extension. + * + * @public + */ +export const RepoBranchPicker = (props: RepoBranchPickerProps) => { + const { + uiSchema, + onChange, + rawErrors, + formData, + schema, + formContext, + required, + } = props; + const { + formData: { repoUrl }, + } = formContext; + + const [state, setState] = useState({ + branch: formData || '', + }); + const { host, branch } = state; + + const integrationApi = useApi(scmIntegrationsApiRef); + const scmAuthApi = useApi(scmAuthApiRef); + + const { secrets, setSecrets } = useTemplateSecrets(); + + useDebounce( + async () => { + const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; + + if (!requestUserCredentials || !host) { + return; + } + + // don't show login prompt if secret value is already in state + if (secrets[requestUserCredentials.secretsKey]) { + return; + } + + // user has requested that we use the users credentials + // so lets grab them using the scmAuthApi and pass through + // any additional scopes from the ui:options + const { token } = await scmAuthApi.getCredentials({ + url: `https://${host}`, + additionalScope: { + repoWrite: true, + customScopes: requestUserCredentials.additionalScopes, + }, + }); + + // set the secret using the key provided in the ui:options for use + // in the templating the manifest with ${{ secrets[secretsKey] }} + setSecrets({ [requestUserCredentials.secretsKey]: token }); + }, + 500, + [host, uiSchema], + ); + + useEffect(() => { + if (repoUrl) { + const url = new URL(`https://${repoUrl}`); + + setState(prevState => ({ + ...prevState, + host: url.host, + workspace: url.searchParams.get('workspace') || '', + repository: url.searchParams.get('repo') || '', + })); + } + }, [repoUrl]); + + useEffect(() => { + onChange(branch); + }, [branch, onChange]); + + const updateLocalState = useCallback( + (newState: RepoBranchPickerState) => { + setState(prevState => ({ ...prevState, ...newState })); + }, + [setState], + ); + + const hostType = (host && integrationApi.byHost(host)?.type) ?? null; + + const renderRepoBranchPicker = () => { + switch (hostType) { + case 'bitbucket': + return ( + + ); + default: + return ( + + ); + } + }; + + return ( + <> + {schema.title && ( + + {schema.title} + + + )} + {schema.description && ( + {schema.description} + )} + {renderRepoBranchPicker()} + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts new file mode 100644 index 0000000000..2b87b62ebd --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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 { + RepoBranchPickerFieldSchema, + type RepoBranchPickerUiOptions, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts new file mode 100644 index 0000000000..fae5e6435e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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 { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const RepoBranchPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + gitea: z + .array(z.string()) + .optional() + .describe('Additional Gitea scopes to request'), + gerrit: z + .array(z.string()) + .optional() + .describe('Additional Gerrit scopes to request'), + github: z + .array(z.string()) + .optional() + .describe('Additional GitHub scopes to request'), + gitlab: z + .array(z.string()) + .optional() + .describe('Additional GitLab scopes to request'), + bitbucket: z + .array(z.string()) + .optional() + .describe('Additional BitBucket scopes to request'), + azure: z + .array(z.string()) + .optional() + .describe('Additional Azure scopes to request'), + }) + .optional() + .describe('Additional permission scopes to request'), + }) + .optional() + .describe( + 'If defined will request user credentials to auth against the given SCM platform', + ), + }), +); + +/** + * The input props that can be specified under `ui:options` for the + * `RepoBranchPicker` field extension. + * + * @public + */ +export type RepoBranchPickerUiOptions = + typeof RepoBranchPickerFieldSchema.uiOptionsType; + +export type RepoBranchPickerProps = typeof RepoBranchPickerFieldSchema.type; + +// This has been duplicated from /plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +// NOTE: There is a bug with this failing validation in the custom field explorer due +// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if +// requestUserCredentials is not defined +export const RepoBranchPickerSchema = RepoBranchPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts new file mode 100644 index 0000000000..bd218d9e28 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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 RepoBranchPickerState { + host?: string; + workspace?: string; + repository?: string; + branch?: string; +} + +export type BaseRepoBranchPickerProps = T & { + onChange: (state: RepoBranchPickerState) => void; + state: RepoBranchPickerState; + rawErrors: string[]; + required?: boolean; +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts index 2a4691380f..2ec72bc5a6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -97,6 +97,7 @@ export type RepoUrlPickerUiOptions = export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type; +// This has been duplicated to /plugins/scaffolder/src/components/fields/RepoBranchPicker/schema.ts // NOTE: There is a bug with this failing validation in the custom field explorer due // to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if // requestUserCredentials is not defined diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index a59f3e08e3..e77d968c80 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -50,6 +50,8 @@ import { MultiEntityPickerSchema, validateMultiEntityPickerValidation, } from '../components/fields/MultiEntityPicker/MultiEntityPicker'; +import { RepoBranchPicker } from '../components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from '../components/fields/RepoBranchPicker/schema'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { @@ -99,4 +101,9 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ schema: MultiEntityPickerSchema, validation: validateMultiEntityPickerValidation, }, + { + component: RepoBranchPicker, + name: 'RepoBranchPicker', + schema: RepoBranchPickerSchema, + }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index d809ec00f9..c5709f1080 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -30,6 +30,7 @@ export { MyGroupsPickerFieldExtension, RepoUrlPickerFieldExtension, MultiEntityPickerFieldExtension, + RepoBranchPickerFieldExtension, ScaffolderPage, scaffolderPlugin, } from './plugin'; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index c053bfacd9..c6c858f6b1 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -73,6 +73,8 @@ import { MyGroupsPicker, MyGroupsPickerSchema, } from './components/fields/MyGroupsPicker/MyGroupsPicker'; +import { RepoBranchPicker } from './components/fields/RepoBranchPicker/RepoBranchPicker'; +import { RepoBranchPickerSchema } from './components/fields/RepoBranchPicker/schema'; /** * The main plugin export for the scaffolder. @@ -231,3 +233,16 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( schema: EntityTagsPickerSchema, }), ); + +/** + * A field extension to select a branch from a repository. + * + * @public + */ +export const RepoBranchPickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: RepoBranchPicker, + name: 'RepoBranchPicker', + schema: RepoBranchPickerSchema, + }), +); diff --git a/plugins/search-backend-module-catalog/CHANGELOG.md b/plugins/search-backend-module-catalog/CHANGELOG.md index 14b950872a..8c8e397620 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json index 9914ab599e..8eda12eac2 100644 --- a/plugins/search-backend-module-catalog/package.json +++ b/plugins/search-backend-module-catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-catalog", - "version": "0.1.29-next.1", + "version": "0.1.29-next.2", "description": "A module for the search backend that exports catalog modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index d851114cc3..f6866f0878 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.5.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/config@1.2.0 + ## 1.5.4-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 56a1dcd6e2..e4f6a80774 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "1.5.4-next.1", + "version": "1.5.4-next.2", "description": "A module for the search backend that implements search using ElasticSearch", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-explore/CHANGELOG.md b/plugins/search-backend-module-explore/CHANGELOG.md index 344969254c..4ea47e64cc 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + ## 0.1.29-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json index 7827067c8a..0b9f84e86e 100644 --- a/plugins/search-backend-module-explore/package.json +++ b/plugins/search-backend-module-explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-explore", - "version": "0.1.29-next.1", + "version": "0.1.29-next.2", "description": "A module for the search backend that exports explore modules", "backstage": { "role": "backend-plugin-module", @@ -55,7 +55,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 03a33c2081..bc3a4e1ef4 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/config@1.2.0 + ## 0.5.33-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 62b5309b06..76e5de5af1 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.5.33-next.1", + "version": "0.5.33-next.2", "description": "A module for the search backend that implements search using PostgreSQL", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md index a3c64851bb..caada51643 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.1.16-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + ## 0.1.16-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 7f66d093d8..91a45a3a65 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", - "version": "0.1.16-next.1", + "version": "0.1.16-next.2", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", @@ -41,7 +41,7 @@ "@backstage/config": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "qs": "^6.9.4" }, "devDependencies": { diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 9f5b9e41da..743b2a32ee 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.28-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + ## 0.1.28-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json index ec703cf38e..92b59c209c 100644 --- a/plugins/search-backend-module-techdocs/package.json +++ b/plugins/search-backend-module-techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-techdocs", - "version": "0.1.28-next.1", + "version": "0.1.28-next.2", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", @@ -61,7 +61,7 @@ "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-techdocs-node": "workspace:^", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.1.0" }, "devDependencies": { diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 9597076338..90c4b9fa8b 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-search-backend-node +## 1.2.28-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.2.28-next.1 ### Patch Changes diff --git a/plugins/search-backend-node/api-report-alpha.md b/plugins/search-backend-node/api-report-alpha.md index 4f3015a0e4..e1d5bd024d 100644 --- a/plugins/search-backend-node/api-report-alpha.md +++ b/plugins/search-backend-node/api-report-alpha.md @@ -46,7 +46,11 @@ export type SearchIndexServiceInitOptions = { }; // @alpha -export const searchIndexServiceRef: ServiceRef; +export const searchIndexServiceRef: ServiceRef< + SearchIndexService, + 'plugin', + 'singleton' +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 710de9cd67..a7c53e61d1 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,12 +1,16 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.2.28-next.1", + "version": "1.2.28-next.2", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", - "pluginId": "search-backend", + "pluginId": "search", "pluginPackages": [ - "@backstage/plugin-search-backend-node" + "@backstage/plugin-search", + "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", + "@backstage/plugin-search-common", + "@backstage/plugin-search-react" ] }, "publishConfig": { diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 16e4c8ae4f..b6f4e9c001 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search-backend +## 1.5.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.5.15-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 8962162af6..105a317a39 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "1.5.15-next.1", + "version": "1.5.15-next.2", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", @@ -8,6 +8,7 @@ "pluginPackages": [ "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", "@backstage/plugin-search-react" ] diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index dc76382d5c..9b6f2ed89c 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-common +## 1.2.14-next.1 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + ## 1.2.14-next.0 ### Patch Changes diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index f569388c48..073f5ead39 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-common", - "version": "1.2.14-next.0", + "version": "1.2.14-next.1", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", "backstage": { "role": "common-library", @@ -8,6 +8,7 @@ "pluginPackages": [ "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", "@backstage/plugin-search-react" ] diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index f3c5688145..4b8307c516 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-search-react +## 1.7.14-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.7.14-next.1 ### Patch Changes diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index c6e011fe41..4bcdbb3f78 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -25,7 +25,15 @@ export function createSearchResultListItemExtension< }, >( options: SearchResultItemExtensionOptions, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export namespace createSearchResultListItemExtension { diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 4fa50cca10..21ee4e4904 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,12 +1,13 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.14-next.1", + "version": "1.7.14-next.2", "backstage": { "role": "web-library", "pluginId": "search", "pluginPackages": [ "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", "@backstage/plugin-search-react" ] diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 5e04d7a198..ad3703a51e 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-search +## 1.4.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.4.15-next.1 ### Patch Changes diff --git a/plugins/search/api-report-alpha.md b/plugins/search/api-report-alpha.md index e9b86f1f87..e6f91d6d19 100644 --- a/plugins/search/api-report-alpha.md +++ b/plugins/search/api-report-alpha.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; @@ -12,12 +13,21 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; // @alpha (undocumented) -export const searchApi: ExtensionDefinition<{}, {}>; +export const searchApi: ExtensionDefinition< + {}, + {}, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export const searchNavItem: ExtensionDefinition< @@ -26,7 +36,12 @@ export const searchNavItem: ExtensionDefinition< }, { title?: string | undefined; - } + }, + never, + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha (undocumented) @@ -38,7 +53,12 @@ export const searchPage: ExtensionDefinition< { path: string; noTrack: boolean; - } + }, + AnyExtensionDataRef, + {}, + string | undefined, + string | undefined, + string | undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/search/package.json b/plugins/search/package.json index a4a5ea3593..9cad64fcd5 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "1.4.15-next.1", + "version": "1.4.15-next.2", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", @@ -8,6 +8,7 @@ "pluginPackages": [ "@backstage/plugin-search", "@backstage/plugin-search-backend", + "@backstage/plugin-search-backend-node", "@backstage/plugin-search-common", "@backstage/plugin-search-react" ] diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index d95a9d74a3..ad5f012dbd 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-signals-backend +## 0.1.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/signals-backend/README.md b/plugins/signals-backend/README.md index d76038a38e..24d8832d26 100644 --- a/plugins/signals-backend/README.md +++ b/plugins/signals-backend/README.md @@ -6,41 +6,4 @@ Signals plugin allows backend plugins to publish messages to frontend plugins. ## Getting started -First install the `@backstage/plugin-signals-node` plugin to get the `SignalsService` set up. - -Next, add Signals router to your backend in `packages/backend/src/plugins/signals.ts`: - -```ts -import { Router } from 'express'; -import { createRouter } from '@backstage/plugin-signals-backend'; -import { PluginEnvironment } from '../types'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - eventBroker: env.eventBroker, - identity: env.identity, - discovery: env.discovery, - }); -} -``` - -Now add the signals to `packages/backend/src/index.ts`: - -```ts -// ... -import signals from './plugins/signals'; - -async function main() { - // ... - const signalsEnv = useHotMemoize(module, () => createEnv('signals')); - - const apiRouter = Router(); - // ... - apiRouter.use('/signals', await signals(signalsEnv)); - apiRouter.use(notFoundHandler()); - // ... -} -``` +To install this signals backend plugin, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json index 6e4a17d612..81627acd13 100644 --- a/plugins/signals-backend/package.json +++ b/plugins/signals-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-backend", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "backstage": { "role": "backend-plugin", "pluginId": "signals", @@ -48,7 +48,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "http-proxy-middleware": "^2.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0", "winston": "^3.2.1", "ws": "^8.17.0", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index ac1b4d835d..c9b4ed0d9f 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-signals-node +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/signals-node/api-report.md b/plugins/signals-node/api-report.md index cf4ab84aff..a368dd2ece 100644 --- a/plugins/signals-node/api-report.md +++ b/plugins/signals-node/api-report.md @@ -37,7 +37,7 @@ export type SignalPayload = { export interface SignalService extends SignalsService {} // @public @deprecated (undocumented) -export const signalService: ServiceRef; +export const signalService: ServiceRef; // @public (undocumented) export interface SignalsService { @@ -52,7 +52,11 @@ export type SignalsServiceOptions = { }; // @public (undocumented) -export const signalsServiceRef: ServiceRef; +export const signalsServiceRef: ServiceRef< + SignalsService, + 'plugin', + 'singleton' +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/signals-node/package.json b/plugins/signals-node/package.json index 7b98587482..7dd3df3902 100644 --- a/plugins/signals-node/package.json +++ b/plugins/signals-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-signals-node", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "description": "Node.js library for the signals plugin", "backstage": { "role": "node-library", diff --git a/plugins/signals/README.md b/plugins/signals/README.md index 38dc18808b..5b08983621 100644 --- a/plugins/signals/README.md +++ b/plugins/signals/README.md @@ -9,23 +9,7 @@ Signals plugin allows backend plugins to publish messages to frontend plugins. This plugin contains client that can receive messages from the backend. To get started, see installation instructions from `@backstage/plugin-signals-node`, `@backstage/plugin-signals-backend`. -To install the plugin, you have to add the following to your `packages/app/src/plugins.ts`: - -```ts -export { signalsPlugin } from '@backstage/plugin-signals'; -``` - -And make sure that your `packages/app/src/App.tsx` contains: - -```ts -import * as plugins from './plugins'; - -const app = createApp({ - // ... - plugins: Object.values(plugins), - // ... -}); -``` +To install this signals frontend plugin, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. Now you can utilize the API from other plugins using the `@backstage/plugin-signals-react` package or simply by: diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index 728f9e073d..e6556fd543 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 1.0.37-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index a13624e2d8..9ef481927e 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.37-next.1", + "version": "1.0.37-next.2", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index cc0383d7f4..0fc47107da 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-techdocs-backend +## 1.10.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + ## 1.10.10-next.1 ### Patch Changes diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 9d1cd9e170..e22fc549d9 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -68,22 +68,14 @@ export interface Config { /** * Techdocs publisher information */ - publisher?: - | { - type: - | 'local' - | 'googleGcs' - | 'awsS3' - | 'azureBlobStorage' - | 'openStackSwift'; - - local?: { - /** - * Directory to store generated static files. - */ - publishDirectory?: string; - }; - } + publisher?: { + local?: { + /** + * Directory to store generated static files. + */ + publishDirectory?: string; + }; + } & ( | { type: 'awsS3'; @@ -132,6 +124,11 @@ export interface Config { * (Required) Cloud Storage Bucket Name */ bucketName: string; + /** + * (Optional) Location in storage bucket to save files + * If not set, the default location will be the root of the storage bucket + */ + bucketRootPath?: string; /** * (Optional) AWS Region. * If not set, AWS_REGION environment variable or aws config file will be used. @@ -261,7 +258,8 @@ export interface Config { */ projectId?: string; }; - }; + } + ); /** * @example http://localhost:7007/api/techdocs diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index f12a32a1ec..9c0355956e 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "1.10.10-next.1", + "version": "1.10.10-next.2", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", @@ -76,7 +76,7 @@ "fs-extra": "^11.2.0", "knex": "^3.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.1.0", "winston": "^3.2.1" }, diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 40c203014e..22cf92a28c 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs-node +## 1.12.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + ## 1.12.9-next.1 ### Patch Changes diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 8553169546..f6fa8a9948 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-node", - "version": "1.12.9-next.1", + "version": "1.12.9-next.2", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "backstage": { "role": "node-library", diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 6ee3870845..695f5b8f2c 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -115,6 +115,7 @@ describe('helpers', () => { url | repo_url | edit_uri ${'https://github.com/backstage/backstage'} | ${'https://github.com/backstage/backstage'} | ${undefined} ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/docs'} + ${'https://github.com/backstage/backstage/tree/main/examples/techdocs'} | ${'https://github.com/backstage/backstage/tree/main/examples/techdocs'} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/docs'} ${'https://github.com/backstage/backstage/tree/main/'} | ${'https://github.com/backstage/backstage/tree/main/'} | ${'https://github.com/backstage/backstage/edit/main/docs'} ${'https://gitlab.com/backstage/backstage'} | ${'https://gitlab.com/backstage/backstage'} | ${undefined} ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${'https://gitlab.com/backstage/backstage/-/edit/main/examples/techdocs/docs'} diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index 5efe96877f..9c1996a772 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -117,7 +117,7 @@ export const getRepoUrlFromLocationAnnotation = ( const sourceFolder = integration.resolveUrl({ url: `./${docsFolder}`, - base: target, + base: target.endsWith('/') ? target : `${target}/`, }); return { repo_url: target, diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 0318bf45aa..237612750d 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-techdocs +## 1.10.8-next.2 + +### Patch Changes + +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + ## 1.10.8-next.1 ### Patch Changes diff --git a/plugins/techdocs/api-report-alpha.md b/plugins/techdocs/api-report-alpha.md index 04a0c5c512..f7c6679139 100644 --- a/plugins/techdocs/api-report-alpha.md +++ b/plugins/techdocs/api-report-alpha.md @@ -18,6 +18,7 @@ const _default: BackstagePlugin< }>; entityContent: RouteRef; }, + {}, {} >; export default _default; @@ -37,7 +38,12 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition< asListItem: boolean; asLink: boolean; title?: string | undefined; - } + }, + never, + never, + string | undefined, + string | undefined, + string | undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 90834c6fcb..781d8db422 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -30,6 +30,7 @@ import { TechDocsApi as TechDocsApi_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsEntityMetadata as TechDocsEntityMetadata_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsMetadata as TechDocsMetadata_2 } from '@backstage/plugin-techdocs-react'; import { TechDocsStorageApi as TechDocsStorageApi_2 } from '@backstage/plugin-techdocs-react'; +import { ThemeOptions } from '@material-ui/core/styles'; import { ToolbarProps } from '@material-ui/core/Toolbar'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; @@ -389,6 +390,7 @@ export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ export type TechDocsReaderPageProps = { entityRef?: CompoundEntityRef; children?: TechDocsReaderPageRenderFunction | ReactNode; + overrideThemeOptions?: Partial; }; // @public diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index c08dd92f09..9ff5e3e2f4 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "1.10.8-next.1", + "version": "1.10.8-next.2", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index ed8b05c661..5e8cd7af27 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -252,4 +252,30 @@ describe('', () => { expect(rendered.getByText('the page')).toBeInTheDocument(); }); + + it('should apply overrideThemeOptions', async () => { + const overrideThemeOptions = { + typography: { fontFamily: 'Comic Sans MS' }, + }; + + const rendered = await renderInTestApp( + + + , + { + mountedRoutes, + }, + ); + + const text = rendered.getAllByText(mockTechDocsMetadata.site_name)[0]; + + expect(text).toHaveStyle('fontFamily: Comic Sans MS'); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index b1b9ecb13b..0f48df7f39 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -37,6 +37,8 @@ import { } from '@backstage/core-plugin-api'; import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react'; +import { ThemeOptions } from '@material-ui/core/styles'; +import { createTheme, ThemeProvider, useTheme } from '@material-ui/core/styles'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -151,6 +153,7 @@ export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => { export type TechDocsReaderPageProps = { entityRef?: CompoundEntityRef; children?: TechDocsReaderPageRenderFunction | ReactNode; + overrideThemeOptions?: Partial; }; /** @@ -159,6 +162,12 @@ export type TechDocsReaderPageProps = { * @public */ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { + const currentTheme = useTheme(); + + const readerPageTheme = createTheme({ + ...currentTheme, + ...(props.overrideThemeOptions || {}), + }); const { kind, name, namespace } = useRouteRefParams(rootDocsRouteRef); const { children, entityRef = { kind, name, namespace } } = props; @@ -179,33 +188,37 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { // As explained above, "page" is configuration 4 and is 1 return ( - - - {(page as JSX.Element) || } - - + + + + {(page as JSX.Element) || } + + + ); } // As explained above, a render function is configuration 3 and React element is 2 return ( - - - {({ metadata, entityMetadata, onReady }) => ( -
    - - {children instanceof Function - ? children({ - entityRef, - techdocsMetadataValue: metadata.value, - entityMetadataValue: entityMetadata.value, - onReady, - }) - : children} - -
    - )} -
    -
    + + + + {({ metadata, entityMetadata, onReady }) => ( +
    + + {children instanceof Function + ? children({ + entityRef, + techdocsMetadataValue: metadata.value, + entityMetadataValue: entityMetadata.value, + onReady, + }) + : children} + +
    + )} +
    +
    +
    ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index de2460a543..c686827255 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -46,6 +46,7 @@ import { useStylesTransformer, } from '../../transformers'; import { useNavigateUrl } from './useNavigateUrl'; +import { handleMetaRedirects } from '../../transformers/handleMetaRedirects'; const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)'; @@ -186,6 +187,7 @@ export const useTechDocsReaderDom = ( const postRender = useCallback( async (transformedElement: Element) => transformer(transformedElement, [ + handleMetaRedirects(navigate, entityRef.name), scrollIntoNavigation(), copyToClipboard(theme), addLinkClickListener({ @@ -243,7 +245,7 @@ export const useTechDocsReaderDom = ( onLoaded: () => {}, }), ]), - [theme, navigate, analytics], + [theme, navigate, analytics, entityRef.name], ); useEffect(() => { diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts new file mode 100644 index 0000000000..12a552577a --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2024 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 { handleMetaRedirects } from './handleMetaRedirects'; +import { createTestShadowDom } from '../../test-utils'; +import { screen } from '@testing-library/react'; + +describe('handleMetaRedirects', () => { + const navigate = jest.fn(); + + const setUpNewTestShadowDom = async ( + html: string, + rootHref: string, + rootPath: string, + ) => { + const entityName = 'testEntity'; + // Mock window.location.href for each test + Object.defineProperty(window, 'location', { + value: { + href: rootHref, + pathname: rootPath, + hostname: 'localhost', + }, + writable: true, + }); + return await createTestShadowDom(html, { + preTransformers: [], + postTransformers: [handleMetaRedirects(navigate, entityName)], + }); + }; + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + document.body.innerHTML = ''; + }); + + it('should navigate to relative URL if meta redirect tag is present', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); + expect(navigate).toHaveBeenCalledWith( + 'http://localhost/docs/default/component/testEntity/anotherPage', + ); + }); + + it('should navigate to site home if meta redirect tag is present and external', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); + expect(navigate).toHaveBeenCalledWith('/docs/default/component/testEntity'); + }); + + it('should navigate to absolute URL if meta redirect tag is present and not external', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); + expect(navigate).toHaveBeenCalledWith('http://localhost/test'); + }); + + it('should not navigate if meta redirect tag is not present', async () => { + await setUpNewTestShadowDom( + ``, + 'http://localhost/docs/default/component/testEntity/subpath', + '/docs/default/component/testEntity/subpath', + ); + + jest.runAllTimers(); + expect(navigate).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx new file mode 100644 index 0000000000..2d74f4cbf5 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2024 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 { Transformer } from './transformer'; +import { normalizeUrl } from './rewriteDocLinks'; +import Snackbar from '@material-ui/core/Snackbar'; +import React, { useState } from 'react'; +import { renderReactElement } from './renderReactElement'; +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/core/styles'; + +type RedirectNotificationProps = { + handleButtonClick: () => void; + message: string; + autoHideDuration: number; +}; + +const useStyles = makeStyles(theme => ({ + button: { + color: theme.palette.primary.light, + textDecoration: 'underline', + }, +})); +const RedirectNotification = ({ + message, + handleButtonClick, + autoHideDuration, +}: RedirectNotificationProps) => { + const classes = useStyles(); + const [open, setOpen] = useState(true); + const handleClose = () => { + setOpen(prev => !prev); + }; + + return ( + + Redirect now + + } + /> + ); +}; + +export const handleMetaRedirects = ( + navigate: (to: string) => void, + entityName: string, +): Transformer => { + const redirectAfterMs = 4000; + const determineRedirectURL = (metaUrl: string) => { + const normalizedCurrentUrl = normalizeUrl(window.location.href); + // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. + const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); + const isExternalRedirect = + absoluteRedirectObj.hostname !== window.location.hostname; + + if (isExternalRedirect) { + const currentTechDocPath = window.location.pathname; + const indexOfSiteHome = currentTechDocPath.indexOf(entityName); + const siteHomePath = currentTechDocPath.slice( + 0, + indexOfSiteHome + entityName.length, + ); + return siteHomePath; + } + // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. + return absoluteRedirectObj.href; + }; + + return dom => { + for (const elem of Array.from(dom.querySelectorAll('meta'))) { + if (elem.getAttribute('http-equiv') === 'refresh') { + const metaContentParameters = elem + .getAttribute('content') + ?.split('url='); + if (!metaContentParameters || metaContentParameters.length < 2) { + continue; + } + const metaUrl = metaContentParameters[1]; + const redirectURL = determineRedirectURL(metaUrl); + const container = document.createElement('div'); + + renderReactElement( + navigate(redirectURL)} + autoHideDuration={redirectAfterMs} + />, + container, + ); + document.body.appendChild(container); + + setTimeout(() => { + navigate(redirectURL); + }, redirectAfterMs); + + return dom; + } + } + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx index ed5fbba085..e45d66c676 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx @@ -81,4 +81,44 @@ describe('Transformers > Html', () => { expect(iframes).toHaveLength(1); expect(iframes[0].src).toMatch('docs.google.com'); }); + + it('should return a function that allows refresh meta tags', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const cleanDom = await result.current(dirtyDom); // calling html transformer + + const metaTags = Array.from( + cleanDom.querySelectorAll('meta'), + ); + + expect(metaTags).toHaveLength(1); + expect(metaTags[0].getAttribute('http-equiv')).toEqual('refresh'); + expect(metaTags[0].getAttribute('content')).toEqual( + '0;url=https://test.com', + ); + }); + + it('should return a function that does not allow non-refresh meta tags', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const cleanDom = await result.current(dirtyDom); // calling html transformer + + const metaTags = Array.from( + cleanDom.querySelectorAll('meta'), + ); + + expect(metaTags).toHaveLength(0); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 61498a37b0..4938a36648 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -44,17 +44,39 @@ export const useSanitizerTransformer = (): Transformer => { const hosts = config?.getOptionalStringArray('allowedIframeHosts'); DOMPurify.addHook('beforeSanitizeElements', removeUnsafeLinks); - const tags = ['link']; + const tags = ['link', 'meta']; if (hosts) { tags.push('iframe'); DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } + // Only allow meta tags if they are used for refreshing the page. They are required for the redirect feature. + DOMPurify.addHook('uponSanitizeElement', (currNode, data) => { + if (data.tagName === 'meta') { + const isMetaRefreshTag = + currNode.getAttribute('http-equiv') === 'refresh' && + currNode.getAttribute('content')?.includes('url='); + if (!isMetaRefreshTag) { + currNode.parentNode?.removeChild(currNode); + } + } + }); + + // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. + DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { + if (currNode.tagName !== 'meta') { + if (data.attrName === 'http-equiv' || data.attrName === 'content') { + currNode.removeAttribute(data.attrName); + } + } + }); + // using outerHTML as we want to preserve the html tag attributes (lang) return DOMPurify.sanitize(dom.outerHTML, { ADD_TAGS: tags, FORBID_TAGS: ['style'], + ADD_ATTR: ['http-equiv', 'content'], WHOLE_DOCUMENT: true, RETURN_DOM: true, }); diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts index cc7ba6bace..c682008a4d 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts @@ -30,6 +30,8 @@ type TypographyHeadings = Pick< type TypographyHeadingsKeys = keyof TypographyHeadings; const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; +const relativeLengthUnit: RegExp = /(em)|(rem)/gi; +const cssVariable: RegExp = /var\(|\)/gi; export default ({ theme }: RuleOptions) => ` /*================== Typeset ==================*/ @@ -43,17 +45,27 @@ ${headings.reduce((style, heading) => { (theme.typography as BackstageTypography).htmlFontSize ?? 16; const styles = theme.typography[heading]; const { lineHeight, fontFamily, fontWeight, fontSize } = styles; - const calculate = (value: typeof fontSize) => { - let factor: number | string = 1; + const calculate = (value: typeof fontSize): string | undefined => { if (typeof value === 'number') { - // convert px to rem - // 60% of the size defined because it is too big - factor = (value / htmlFontSize) * 0.6; + // Convert px to rem and apply 60% factor + return calculate(`${(value / htmlFontSize) * 0.6}rem`); + } else if (typeof value === 'string') { + if (value.match(cssVariable)) { + // Resolve css variable and calculate recursively + const resolvedValue = window + .getComputedStyle(document.body) + .getPropertyValue(value.replaceAll(cssVariable, '')); + if (resolvedValue !== '') { + return calculate(resolvedValue); + } + } else if (value.match(relativeLengthUnit)) { + // Use relative size as factor + const factor = value.replace(relativeLengthUnit, ''); + return `calc(${factor} * var(--md-typeset-font-size))`; + } } - if (typeof value === 'string') { - factor = value.replace('rem', ''); - } - return `calc(${factor} * var(--md-typeset-font-size))`; + // Value is not a number, relative length unit, or CSS variable, return as is + return value; }; return style.concat(` .md-typeset ${heading} { diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts b/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts deleted file mode 100644 index 90244e96ad..0000000000 --- a/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts +++ /dev/null @@ -1,48 +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 { renderHook } from '@testing-library/react'; -import { useStylesTransformer } from './transformer'; - -describe('Transformers > Styles', () => { - it('should return a function that injects all styles into a given dom element', () => { - const { result } = renderHook(() => useStylesTransformer()); - - const dom = document.createElement('html'); - dom.innerHTML = ''; - result.current(dom); // calling styles transformer - - const style = dom.querySelector('head > style'); - expect(style).toHaveTextContent( - '/*================== Variables ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Reset ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Layout ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Typeset ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Animations ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Extensions ==================*/', - ); - }); -}); diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx new file mode 100644 index 0000000000..b55e372212 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { useStylesTransformer } from './transformer'; +import { createTheme, ThemeProvider } from '@material-ui/core/styles'; +import React from 'react'; + +describe('Transformers > Styles', () => { + it('should return a function that injects all styles into a given dom element', () => { + const { result } = renderHook(() => useStylesTransformer()); + + const dom = document.createElement('html'); + dom.innerHTML = ''; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + + expect(style).toHaveTextContent( + '/*================== Variables ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Reset ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Layout ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Typeset ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Animations ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Extensions ==================*/', + ); + }); + + it('should use headers relative font-size value as the factor for the md-typeset variable', () => { + const theme = createTheme({ + typography: { + h1: { + fontSize: '20rem', + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + 'font-size: calc(20 * var(--md-typeset-font-size));', + ); + }); + + it('should resolve header sizes that are variables', () => { + document.body.style.setProperty('--font-size-h1', '20rem'); + const theme = createTheme({ + typography: { + h1: { + fontSize: 'var(--font-size-h1)', + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + 'font-size: calc(20 * var(--md-typeset-font-size));', + ); + }); + + it('should convert pixel header sizes to REM and reduce by 60%', () => { + const theme = createTheme({ + typography: { + htmlFontSize: 16, + h1: { + fontSize: 100, + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + // 100px / 16px * 0.6 = 3.75rem + 'font-size: calc(3.75 * var(--md-typeset-font-size));', + ); + }); +}); diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 9101a22bef..144d27d627 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-user-settings-backend +## 0.2.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.2.22-next.1 ### Patch Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 244209a267..808f59fec4 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.2.22-next.1", + "version": "0.2.22-next.2", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index dafa984b09..a1aaa74828 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-user-settings +## 0.8.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.8.11-next.1 ### Patch Changes diff --git a/plugins/user-settings/api-report-alpha.md b/plugins/user-settings/api-report-alpha.md index fdb2c5f6bf..700e06a7ad 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -13,6 +13,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; @@ -24,7 +25,12 @@ export const settingsNavItem: ExtensionDefinition< }, { title?: string | undefined; - } + }, + never, + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha (undocumented) diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index b229e625b6..4217cf3329 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.11-next.1", + "version": "0.8.11-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/yarn.lock b/yarn.lock index b321863309..c14251269c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3503,7 +3503,7 @@ __metadata: minimist: ^1.2.5 morgan: ^1.10.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 path-to-regexp: ^6.2.1 selfsigned: ^2.0.0 @@ -3588,7 +3588,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 mysql2: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 p-limit: ^3.1.0 path-to-regexp: ^6.2.1 @@ -3677,7 +3677,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 mysql2: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 p-limit: ^3.1.0 path-to-regexp: ^6.2.1 @@ -4009,7 +4009,7 @@ __metadata: mini-css-extract-plugin: ^2.4.2 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-libs-browser: ^2.2.1 nodemon: ^3.0.1 npm-packlist: ^5.0.0 @@ -4103,7 +4103,7 @@ __metadata: lodash: ^4.17.21 minimist: ^1.2.5 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 typescript-json-schema: ^0.63.0 yaml: ^2.0.0 zen-observable: ^0.10.0 @@ -4171,6 +4171,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-catalog": "workspace:^" + "@backstage/test-utils": "workspace:^" "@backstage/version-bridge": "workspace:^" "@oriflame/backstage-plugin-score-card": ^0.8.0 "@testing-library/jest-dom": ^6.0.0 @@ -4395,7 +4396,7 @@ __metadata: handlebars: ^4.7.3 inquirer: ^8.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 nodemon: ^3.0.1 ora: ^5.3.0 recursive-readdir: ^2.2.2 @@ -4551,6 +4552,7 @@ __metadata: resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils" dependencies: "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -4695,7 +4697,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" - "@graphiql/react": ^0.20.0 + "@graphiql/react": ^0.23.0 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 @@ -4745,7 +4747,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.3 yn: ^4.0.0 languageName: unknown @@ -4815,7 +4817,7 @@ __metadata: jose: ^5.0.0 msw: ^2.0.8 node-cache: ^5.1.2 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 languageName: unknown linkType: soft @@ -4870,7 +4872,7 @@ __metadata: express: ^4.18.2 jose: ^5.0.0 msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-mocks-http: ^1.0.0 uuid: ^9.0.0 languageName: unknown @@ -4971,7 +4973,7 @@ __metadata: express: ^4.18.2 jose: ^5.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 passport-microsoft: ^1.0.0 supertest: ^6.3.3 languageName: unknown @@ -5170,7 +5172,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 node-cache: ^5.1.2 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 openid-client: ^5.2.1 passport: ^0.7.0 passport-auth0: ^1.4.3 @@ -5207,7 +5209,7 @@ __metadata: jose: ^5.0.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 passport: ^0.7.0 supertest: ^6.1.3 uuid: ^9.0.0 @@ -5300,7 +5302,7 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5366,7 +5368,7 @@ __metadata: "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5405,7 +5407,7 @@ __metadata: fs-extra: ^11.2.0 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5452,7 +5454,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5496,7 +5498,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5582,7 +5584,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.0.2 qs: ^6.9.4 uuid: ^9.0.0 @@ -5629,7 +5631,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5712,7 +5714,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.0.2 prom-client: ^15.0.0 supertest: ^6.1.3 @@ -6063,7 +6065,7 @@ __metadata: express-promise-router: ^4.1.0 fs-extra: ^11.0.0 lodash: ^4.17.21 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 ping: ^0.4.1 semver: ^7.5.3 supertest: ^6.2.4 @@ -6338,7 +6340,7 @@ __metadata: luxon: ^3.0.0 morgan: ^1.10.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 stream-buffers: ^3.0.2 supertest: ^6.1.3 winston: ^3.2.1 @@ -6411,7 +6413,7 @@ __metadata: "@backstage/types": "workspace:^" "@kubernetes/client-node": ^0.20.0 msw: ^1.3.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.3 winston: ^3.2.1 languageName: unknown @@ -6545,7 +6547,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.2.4 uuid: ^9.0.0 winston: ^3.2.1 @@ -6578,7 +6580,7 @@ __metadata: "@backstage/test-utils": "workspace:^" knex: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -6720,7 +6722,7 @@ __metadata: express-promise-router: ^4.1.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.6 yn: ^4.0.0 zod: ^3.22.4 @@ -6840,7 +6842,7 @@ __metadata: http-proxy-middleware: ^2.0.0 morgan: ^1.10.0 msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 @@ -6880,7 +6882,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6899,7 +6901,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6920,7 +6922,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6941,7 +6943,7 @@ __metadata: fs-extra: ^11.2.0 git-url-parse: ^14.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-html-markdown: ^1.3.0 yaml: ^2.0.0 languageName: unknown @@ -7000,7 +7002,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -7018,7 +7020,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -7123,7 +7125,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.3.3 languageName: unknown linkType: soft @@ -7455,7 +7457,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.2.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 languageName: unknown linkType: soft @@ -7490,7 +7492,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.2.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 qs: ^6.9.4 languageName: unknown linkType: soft @@ -7515,7 +7517,7 @@ __metadata: "@backstage/plugin-techdocs-node": "workspace:^" lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.1.0 languageName: unknown linkType: soft @@ -7686,7 +7688,7 @@ __metadata: express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.2.4 uuid: ^9.0.0 winston: ^3.2.1 @@ -7809,7 +7811,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.1.0 supertest: ^6.1.3 winston: ^3.2.1 @@ -9611,7 +9613,7 @@ __metadata: languageName: node linkType: hard -"@graphiql/react@npm:^0.20.0, @graphiql/react@npm:^0.20.3": +"@graphiql/react@npm:^0.20.3": version: 0.20.3 resolution: "@graphiql/react@npm:0.20.3" dependencies: @@ -9638,9 +9640,36 @@ __metadata: languageName: node linkType: hard -"@graphiql/toolkit@npm:^0.9.1": - version: 0.9.1 - resolution: "@graphiql/toolkit@npm:0.9.1" +"@graphiql/react@npm:^0.23.0": + version: 0.23.0 + resolution: "@graphiql/react@npm:0.23.0" + dependencies: + "@graphiql/toolkit": ^0.9.2 + "@headlessui/react": ^1.7.15 + "@radix-ui/react-dialog": ^1.0.4 + "@radix-ui/react-dropdown-menu": ^2.0.5 + "@radix-ui/react-tooltip": ^1.0.6 + "@radix-ui/react-visually-hidden": ^1.0.3 + "@types/codemirror": ^5.60.8 + clsx: ^1.2.1 + codemirror: ^5.65.3 + codemirror-graphql: ^2.0.13 + copy-to-clipboard: ^3.2.0 + framer-motion: ^6.5.1 + graphql-language-service: ^5.2.2 + markdown-it: ^14.1.0 + set-value: ^4.1.0 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: a62ee7d763d410f5ff8c5670596194069e97f11b906762c4e38c74797e4e34c33cd91e06aae24d5118e0fa05dc6c1ffa6c3d3d8ff5bc2b6af0b689088236f613 + languageName: node + linkType: hard + +"@graphiql/toolkit@npm:^0.9.1, @graphiql/toolkit@npm:^0.9.2": + version: 0.9.2 + resolution: "@graphiql/toolkit@npm:0.9.2" dependencies: "@n1ru4l/push-pull-async-iterable-iterator": ^3.1.0 meros: ^1.1.4 @@ -9650,7 +9679,7 @@ __metadata: peerDependenciesMeta: graphql-ws: optional: true - checksum: 5328426051b7f9a9ffbd569c950d1a103ce0e2ee7b5d7a57f3d899488ad43d1a5101e8aeced7416e106c7687d67bb7981aa7e87dea5b0f17b77569aa738bf3b5 + checksum: 4474a998a948d0df5cef8371245b0e895be40803a07e49231ab91798aa761880e6e4a684478a44b723c013f0e8206dab3d86e5b0fc9b688df035fa9f1fa29734 languageName: node linkType: hard @@ -23201,17 +23230,17 @@ __metadata: languageName: node linkType: hard -"codemirror-graphql@npm:^2.0.10": - version: 2.0.11 - resolution: "codemirror-graphql@npm:2.0.11" +"codemirror-graphql@npm:^2.0.10, codemirror-graphql@npm:^2.0.13": + version: 2.0.13 + resolution: "codemirror-graphql@npm:2.0.13" dependencies: "@types/codemirror": ^0.0.90 - graphql-language-service: 5.2.0 + graphql-language-service: 5.2.2 peerDependencies: "@codemirror/language": 6.0.0 codemirror: ^5.65.3 graphql: ^15.5.0 || ^16.0.0 - checksum: cdbeb713af63a069c5526f864edf4f71dd811437e44dba7967164ea2d380c52dfe51d3ea3fde06c94ffeb596b93af737767923d6fc434c628fa4241621684950 + checksum: 7ecb2ba2716448b3c10ccc6168dbe4c636d2d82695beea40cda6f4938b4821e9abcf6446c99412bf59558340f10b45e5b74dce5f80ae4be4409b8a6b7bfcc5ec languageName: node linkType: hard @@ -24718,6 +24747,13 @@ __metadata: languageName: node linkType: hard +"debounce-promise@npm:^3.1.2": + version: 3.1.2 + resolution: "debounce-promise@npm:3.1.2" + checksum: 29bac4524c423cc852319d7455363909ea3d933a3b9e3eb1149d963cffc34c475fe37219d0bafc61af566500b5d663cba579bbad7ee4023bef06f8394ed900ad + languageName: node + linkType: hard + "debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -28886,17 +28922,18 @@ __metadata: languageName: node linkType: hard -"graphql-language-service@npm:5.2.0, graphql-language-service@npm:^5.2.0": - version: 5.2.0 - resolution: "graphql-language-service@npm:5.2.0" +"graphql-language-service@npm:5.2.2, graphql-language-service@npm:^5.2.0, graphql-language-service@npm:^5.2.2": + version: 5.2.2 + resolution: "graphql-language-service@npm:5.2.2" dependencies: + debounce-promise: ^3.1.2 nullthrows: ^1.0.0 vscode-languageserver-types: ^3.17.1 peerDependencies: graphql: ^15.5.0 || ^16.0.0 bin: graphql: dist/temp-bin.js - checksum: b053c6b7158d0ee7a3e55391bfd8be956fc5380211ca586b3a252007845e119540fb40efcc438975eaebc5ef25f46973f7ff4d9543c66e14ebd992957e0299b7 + checksum: 3f112ba67ae10096ac4a415e946cddad019ce9bd5467e29730fdfe08b4470f5b4126412edd6bfcdf6cfbd816c65e295da21134ae501e577540b319f521bc032d languageName: node linkType: hard @@ -32741,6 +32778,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: ^2.0.0 + checksum: b0b86cadaf816b64c947a83994ceaad1c15f9fe7e079776ab88699fb71afd7b8fc3fd3d0ae5ebec8c92c1d347be9ba257b8aef338c0ebf81b0d27dcf429a765a + languageName: node + linkType: hard + "linkify-react@npm:4.1.3": version: 4.1.3 resolution: "linkify-react@npm:4.1.3" @@ -33523,6 +33569,22 @@ __metadata: languageName: node linkType: hard +"markdown-it@npm:^14.1.0": + version: 14.1.0 + resolution: "markdown-it@npm:14.1.0" + dependencies: + argparse: ^2.0.1 + entities: ^4.4.0 + linkify-it: ^5.0.0 + mdurl: ^2.0.0 + punycode.js: ^2.3.1 + uc.micro: ^2.1.0 + bin: + markdown-it: bin/markdown-it.mjs + checksum: 07296b45ebd0b13a55611a24d1b1ad002c6729ec54f558f597846994b0b7b1de79d13cd99ff3e7b6e9e027f36b63125cdcf69174da294ecabdd4e6b9fff39e5d + languageName: node + linkType: hard + "markdown-table@npm:^3.0.0": version: 3.0.1 resolution: "markdown-table@npm:3.0.1" @@ -33767,6 +33829,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 880bc289ef668df0bb34c5b2b5aaa7b6ea755052108cdaf4a5e5968ad01cf27e74927334acc9ebcc50a8628b65272ae6b1fd51fae1330c130e261c0466e1a3b2 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -38135,6 +38204,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 13466d7ed5e8dacdab8c4cc03837e7dd14218a59a40eb14a837f1f53ca396e18ef2c4ee6d7766b8ed2fc391d6a3ac489eebf2de83b3596f5a54e86df4a251b72 + languageName: node + linkType: hard + "punycode@npm:1.3.2": version: 1.3.2 resolution: "punycode@npm:1.3.2" @@ -43421,6 +43497,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 37197358242eb9afe367502d4638ac8c5838b78792ab218eafe48287b0ed28aaca268ec0392cc5729f6c90266744de32c06ae938549aee041fc93b0f9672d6b2 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.0 resolution: "uglify-js@npm:3.17.0"