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/angry-mice-juggle.md b/.changeset/angry-mice-juggle.md new file mode 100644 index 0000000000..c5c90b7365 --- /dev/null +++ b/.changeset/angry-mice-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Export `RelatedEntitiesCard` presets to be reused. diff --git a/.changeset/big-countries-wait.md b/.changeset/big-countries-wait.md new file mode 100644 index 0000000000..15c48b6d44 --- /dev/null +++ b/.changeset/big-countries-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add an extra bit of height to the EntityPicker dropdown to make it clear there are more options to select from, and to remove the scroll bar when there is less than 10 options 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/brown-mails-drum.md b/.changeset/brown-mails-drum.md new file mode 100644 index 0000000000..97d57ff394 --- /dev/null +++ b/.changeset/brown-mails-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING**: Removed several deprecated service factories. These can instead be imported from `@backstage/backend-defaults` package. 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/chilly-numbers-chew.md b/.changeset/chilly-numbers-chew.md new file mode 100644 index 0000000000..3ee9e7837f --- /dev/null +++ b/.changeset/chilly-numbers-chew.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Renamed `createPlugin` to `createFrontendPlugin`. The old symbol is still exported but deprecated. diff --git a/.changeset/clean-kids-applaud.md b/.changeset/clean-kids-applaud.md new file mode 100644 index 0000000000..907b5978cd --- /dev/null +++ b/.changeset/clean-kids-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Fixed issue with notification reloading on page change diff --git a/.changeset/cuddly-penguins-glow.md b/.changeset/cuddly-penguins-glow.md new file mode 100644 index 0000000000..b00422cf8b --- /dev/null +++ b/.changeset/cuddly-penguins-glow.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +`ProcessorOutputCollector` returns an error when receiving deferred entities that have an invalid `metadata.annotations` format. + +This allows to return an error on an actual validation issue instead of reporting that the location annotations are missing afterwards, which is misleading for the users. diff --git a/.changeset/curly-cars-relax.md b/.changeset/curly-cars-relax.md new file mode 100644 index 0000000000..de1b4c3d89 --- /dev/null +++ b/.changeset/curly-cars-relax.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-catalog': patch +--- + +Deprecate the old pattern of `create*Extension`, and replace it with the equivalent Blueprint implementation instead. diff --git a/.changeset/curly-clouds-shave.md b/.changeset/curly-clouds-shave.md new file mode 100644 index 0000000000..0e30f4bba1 --- /dev/null +++ b/.changeset/curly-clouds-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Correct the `TConfig` type of data references to only contain config 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/dull-cheetahs-clap.md b/.changeset/dull-cheetahs-clap.md new file mode 100644 index 0000000000..a2ea657f2e --- /dev/null +++ b/.changeset/dull-cheetahs-clap.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/backend-dynamic-feature-service': patch +'@backstage/plugin-scaffolder-backend-module-rails': patch +'@backstage/plugin-catalog-backend-module-openapi': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-scaffolder-node': patch +'@backstage/plugin-techdocs-node': patch +--- + +Update deprecated url-reader-related imports. diff --git a/.changeset/eight-moose-pull.md b/.changeset/eight-moose-pull.md new file mode 100644 index 0000000000..c5bc5ee66e --- /dev/null +++ b/.changeset/eight-moose-pull.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +`Liveness Probe` added in ContainerCard Component of PodDrawer 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-kangaroos-return.md b/.changeset/fair-kangaroos-return.md new file mode 100644 index 0000000000..6c5b2e57ae --- /dev/null +++ b/.changeset/fair-kangaroos-return.md @@ -0,0 +1,15 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING** Deleted the following deprecated `UrlReader` exports + +- ReadUrlOptions: Use `UrlReaderServiceReadUrlOptions` instead; +- ReadUrlResponse: Use `UrlReaderServiceReadUrlResponse` instead; +- ReadTreeOptions: Use `UrlReaderServiceReadTreeOptions` instead; +- ReadTreeResponse: Use `UrlReaderServiceReadTreeResponse` instead; +- ReadTreeResponseFile: Use `UrlReaderServiceReadTreeResponseFile` instead; +- ReadTreeResponseDirOptions: Use `UrlReaderServiceReadTreeResponseDirOptions` instead; +- SearchOptions: Use `UrlReaderServiceSearchOptions` instead; +- SearchResponse: Use `UrlReaderServiceSearchResponse` instead; +- SearchResponseFile: Use `UrlReaderServiceSearchResponseFile` instead. 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/five-mangos-agree.md b/.changeset/five-mangos-agree.md new file mode 100644 index 0000000000..f5ecb2891b --- /dev/null +++ b/.changeset/five-mangos-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Add ability to set the initial commit message when initializing a repository using the scaffolder action. diff --git a/.changeset/flat-guests-behave.md b/.changeset/flat-guests-behave.md new file mode 100644 index 0000000000..ad4b238e03 --- /dev/null +++ b/.changeset/flat-guests-behave.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +'@backstage/plugin-notifications-backend': patch +--- + +Avoid sending broadcast emails as a fallback in case the entity-typed notification user can not be resolved. 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/forty-geckos-change.md b/.changeset/forty-geckos-change.md new file mode 100644 index 0000000000..484a485670 --- /dev/null +++ b/.changeset/forty-geckos-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +`ConsumingComponentsCard` and `ProvidingComponentsCard` will now optionally accept `columns` to override which table columns are displayed diff --git a/.changeset/four-singers-fetch.md b/.changeset/four-singers-fetch.md new file mode 100644 index 0000000000..69fe6f77bf --- /dev/null +++ b/.changeset/four-singers-fetch.md @@ -0,0 +1,10 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/plugin-kubernetes-node': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/backend-dynamic-feature-service': patch +--- + +Update internal imports diff --git a/.changeset/fresh-timers-walk.md b/.changeset/fresh-timers-walk.md new file mode 100644 index 0000000000..a187bede83 --- /dev/null +++ b/.changeset/fresh-timers-walk.md @@ -0,0 +1,20 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Support overriding of plugin extensions using the new `plugin.withOverrides` method. + +```tsx +import homePlugin from '@backstage/plugin-home'; + +export default homePlugin.withOverrides({ + extensions: [ + homePage.getExtension('page:home').override({ + *factory(originalFactory) { + yield* originalFactory(); + yield coreExtensionData.reactElement(

My custom home page

); + }, + }), + ], +}); +``` 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/friendly-points-approve.md b/.changeset/friendly-points-approve.md new file mode 100644 index 0000000000..52de593750 --- /dev/null +++ b/.changeset/friendly-points-approve.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added deprecation warning to urge users to perform the auth service migration or implement their own token manager service. +See https://backstage.io/docs/tutorials/auth-service-migration for more information. 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-tips-push.md b/.changeset/funny-tips-push.md new file mode 100644 index 0000000000..224e6a073a --- /dev/null +++ b/.changeset/funny-tips-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +update the `morgan` middleware to use a custom format to prevent PII from being logged 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/giant-buttons-guess.md b/.changeset/giant-buttons-guess.md new file mode 100644 index 0000000000..7a7842259e --- /dev/null +++ b/.changeset/giant-buttons-guess.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-app-visualizer': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-org': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-user-settings': patch +--- + +Deprecate the old pattern of `create*Extension`, and replace it with the equivalent Blueprint implementation instead diff --git a/.changeset/healthy-carpets-reflect.md b/.changeset/healthy-carpets-reflect.md new file mode 100644 index 0000000000..ac7734a448 --- /dev/null +++ b/.changeset/healthy-carpets-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +Remove deprecated `urlReaderServiceFactory`, please import from `@backstage/backend-defaults/urlReader` instead. 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/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/large-monkeys-dress.md b/.changeset/large-monkeys-dress.md new file mode 100644 index 0000000000..369a3994c5 --- /dev/null +++ b/.changeset/large-monkeys-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added OpenTelemetry support to Scaffolder metrics diff --git a/.changeset/late-timers-sort.md b/.changeset/late-timers-sort.md new file mode 100644 index 0000000000..d6f5544973 --- /dev/null +++ b/.changeset/late-timers-sort.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-test-utils': minor +--- + +**BREAKING**: Removed these deprecated helpers: + +- `setupRequestMockHandlers` Use `CreateMockDirectoryOptions` instead. +- `CreateMockDirectoryOptions` Use `registerMswTestHooks` instead. + +Stopped exporting the deprecated and internal `isDockerDisabledForTests` helper. + +Removed `get` method from `ServiceFactoryTester` which is replaced by `getSubject` diff --git a/.changeset/lovely-planes-shop.md b/.changeset/lovely-planes-shop.md new file mode 100644 index 0000000000..3ee5a57e75 --- /dev/null +++ b/.changeset/lovely-planes-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +fix: consider broadcast union with user diff --git a/.changeset/lovely-ravens-judge.md b/.changeset/lovely-ravens-judge.md new file mode 100644 index 0000000000..43611556a1 --- /dev/null +++ b/.changeset/lovely-ravens-judge.md @@ -0,0 +1,36 @@ +--- +'@backstage/backend-common': minor +--- + +**BREAKING**: Removed the following `Url Reader` deprecated exports: + +- UrlReader: Use `UrlReaderService` from `@backstage/backend-plugin-api` instead; +- AzureUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- BitbucketUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- BitbucketCloudUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- BitbucketServerUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- GithubUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- GitlabUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- GerritUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- GiteaUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- HarnessUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- AwsS3UrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- FetchUrlReader: Import from `@backstage/backend-defaults/urlReader` instead; +- UrlReaders: Import from `@backstage/backend-defaults/urlReader` instead; +- UrlReadersOptions: Import from `@backstage/backend-defaults/urlReader` instead; +- UrlReaderPredicateTuple: Import from `@backstage/backend-defaults/urlReader` instead; +- FromReadableArrayOptions: Import from `@backstage/backend-defaults/urlReader` instead; +- ReaderFactory: Import from `@backstage/backend-defaults/urlReader` instead; +- ReadUrlOptions:Use `UrlReaderServiceReadUrlOptions` from `@backstage/backend-plugin-api` instead; +- ReadUrlResponse: Use `UrlReaderServiceReadUrlResponse` from `@backstage/backend-plugin-api` instead; +- ReadUrlResponseFactory: Import from `@backstage/backend-defaults/urlReader` instead; +- ReadUrlResponseFactoryFromStreamOptions: Import from `@backstage/backend-defaults/urlReader` instead; +- ReadTreeOptions: Use `UrlReaderServiceReadTreeOptions` from `@backstage/backend-plugin-api` instead; +- ReadTreeResponse: Use `UrlReaderServiceReadTreeResponse` from `@backstage/backend-plugin-api` instead; +- ReadTreeResponseFile: Use `UrlReaderServiceReadTreeResponseFile` from `@backstage/backend-plugin-api` instead; +- ReadTreeResponseDirOptions: Use `UrlReaderServiceReadTreeResponseDirOptions` from `@backstage/backend-plugin-api` instead; +- ReadTreeResponseFactory: Import from `@backstage/backend-defaults/urlReader` instead; +- ReadTreeResponseFactoryOptions: Import from `@backstage/backend-defaults/urlReader` instead; +- SearchOptions: Use `UrlReaderServiceSearchOptions` from `@backstage/backend-plugin-api` instead; +- SearchResponse: Use `UrlReaderServiceSearchResponse` from `@backstage/backend-plugin-api` instead; +- SearchResponseFile: Use `UrlReaderServiceSearchResponseFile` from `@backstage/backend-plugin-api` instead. 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/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-apes-join.md b/.changeset/modern-apes-join.md new file mode 100644 index 0000000000..452896b924 --- /dev/null +++ b/.changeset/modern-apes-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Added back `type: 'local'` to TechDocs config schema for `publisher` diff --git a/.changeset/modern-brooms-grow.md b/.changeset/modern-brooms-grow.md new file mode 100644 index 0000000000..0aff21dcca --- /dev/null +++ b/.changeset/modern-brooms-grow.md @@ -0,0 +1,10 @@ +--- +'@backstage/core-compat-api': patch +'@backstage/frontend-app-api': patch +'@backstage/frontend-test-utils': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-home': patch +'@backstage/plugin-kubernetes': patch +--- + +Migrating usages of the deprecated `createExtension` `v1` format to the newer `v2` format, and old `create*Extension` extension creators to blueprints. 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/new-knives-fly.md b/.changeset/new-knives-fly.md new file mode 100644 index 0000000000..f9ac945cad --- /dev/null +++ b/.changeset/new-knives-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dev-utils': patch +--- + +Fix `EventEmitter` memory leak in the development utilities 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/perfect-cars-jam.md b/.changeset/perfect-cars-jam.md new file mode 100644 index 0000000000..45166ac823 --- /dev/null +++ b/.changeset/perfect-cars-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Changed the way to display entities in `MyGroupsPicker` to use `entityPresentationApi` and make it consistent across scaffolder pickers diff --git a/.changeset/pink-bobcats-explain.md b/.changeset/pink-bobcats-explain.md new file mode 100644 index 0000000000..0274e55369 --- /dev/null +++ b/.changeset/pink-bobcats-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Refactor TechDocs' mkdocs-redirects support. 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..335764699a 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.99", @@ -190,41 +190,73 @@ "@backstage/plugin-techdocs-common": "0.0.0" }, "changesets": [ + "angry-dolphins-camp", + "big-countries-wait", "big-eagles-grab", "blue-pumas-cheer", + "breezy-cats-kiss", "breezy-jeans-tie", + "breezy-rings-fly", + "bright-donkeys-buy", "bright-trainers-brake", + "calm-crabs-drop", + "chilly-days-peel", "chilly-trains-sleep", + "clean-kids-applaud", + "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", + "five-mangos-agree", + "flat-papayas-push", "flat-plums-grow", "forty-ties-agree", "forty-ties-disagree", + "fresh-timers-walk", + "friendly-cherries-applaud", + "friendly-chicken-cry", "friendly-feet-refuse", + "funny-bears-sort", + "funny-wolves-learn", "gentle-dryers-smile", "good-steaks-report", "green-planets-reflect", "grumpy-owls-suffer", + "healthy-fireants-jump", + "healthy-timers-divide", "heavy-numbers-love", + "hip-fishes-guess", "hip-hairs-exist", + "itchy-experts-tie", + "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", + "modern-poems-mate", "neat-bears-divide", "neat-gifts-join", + "neat-socks-cheer", "new-scissors-try", "nice-peas-retire", "nine-cherries-decide", @@ -232,20 +264,41 @@ "nine-seahorses-relate", "ninety-icons-smile", "odd-books-share", + "old-tools-smell", "olive-books-sort", "orange-gifts-protect", + "perfect-cars-jam", "pink-gorillas-brake", "plenty-tools-exist", + "popular-panthers-hear", + "purple-carrots-crash", "quick-roses-juggle", "rare-foxes-compete", + "real-lizards-sit", "red-radios-promise", + "renovate-147ac48", "renovate-f04beb1", "rich-mugs-dress", "selfish-bees-think", + "seven-days-film", "seven-eggs-admire", "shaggy-dodos-applaud", + "shaggy-mugs-return", + "shy-games-poke", + "shy-waves-share", + "silly-candles-sin", + "silly-cycles-tan", + "silly-scissors-turn", + "silver-pillows-begin", + "six-mails-smell", "six-rats-kick", + "sixty-kiwis-poke", + "slow-ducks-rush", + "slow-ligers-drum", + "slow-rocks-end", "slow-toes-jog", + "small-bottles-cough", + "small-ears-poke", "small-spoons-shout", "smooth-countries-relate", "soft-gorillas-refuse", @@ -253,20 +306,38 @@ "spicy-planets-provide", "spotty-planets-accept", "strange-papayas-beg", + "strong-chicken-kiss", "strong-otters-compete", "stupid-dots-relate", "sweet-oranges-buy", "swift-kings-sparkle", "tall-snakes-fix", + "tasty-ads-rescue", + "ten-penguins-roll", "thick-hotels-know", + "thick-squids-drive", + "thin-carrots-eat", + "thirty-adults-grab", "thirty-paws-hope", + "tiny-dodos-prove-2", + "tiny-dodos-prove", "tiny-oranges-pretend", + "tough-goats-hang", "tough-lies-repair", + "tricky-ducks-juggle", + "tricky-rules-destroy", "two-emus-work", "violet-jokes-wave", + "warm-monkeys-marry", + "weak-jobs-joke", "wicked-bobcats-teach", "wild-eggs-exist", + "wise-spiders-walk", "witty-bears-behave", + "witty-geese-battle", + "witty-queens-run", + "witty-timers-marry", + "young-birds-push", "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/rare-maps-wave.md b/.changeset/rare-maps-wave.md new file mode 100644 index 0000000000..7d3e074eae --- /dev/null +++ b/.changeset/rare-maps-wave.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/core-components': patch +--- + +Corrected the documentation for the GCP IAP auth module and updated the configuration to follow proxy configuration conventions by ignoring authEnv 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-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/sixty-kiwis-poke.md b/.changeset/sixty-kiwis-poke.md new file mode 100644 index 0000000000..5281c6ade0 --- /dev/null +++ b/.changeset/sixty-kiwis-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +use formatted query to prevent chance of SQL-injection 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-rocks-end.md b/.changeset/slow-rocks-end.md new file mode 100644 index 0000000000..f2633c9924 --- /dev/null +++ b/.changeset/slow-rocks-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `@module-federation/enhanced` to `0.3.1` 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/strong-chicken-kiss.md b/.changeset/strong-chicken-kiss.md new file mode 100644 index 0000000000..26f667c5ab --- /dev/null +++ b/.changeset/strong-chicken-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': minor +--- + +Support for Cloudflare Custom Headers and Custom Cookie Auth Name 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/three-kiwis-turn.md b/.changeset/three-kiwis-turn.md new file mode 100644 index 0000000000..eb67cbbbdc --- /dev/null +++ b/.changeset/three-kiwis-turn.md @@ -0,0 +1,19 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Deprecate the `.render` method of the `createExtensionTester` in favour of using `renderInTestApp` directly. + +```tsx +import { + renderInTestApp, + createExtensionTester, +} from '@backstage/frontend-test-utils'; + +const tester = createExtensionTester(extension); + +const { getByTestId } = renderInTestApp(tester.reactElement()); + +// or if you're not using `coreExtensionData.reactElement` as the output ref +const { getByTestId } = renderInTestApp(tester.get(myComponentRef)); +``` diff --git a/.changeset/tidy-guests-battle.md b/.changeset/tidy-guests-battle.md new file mode 100644 index 0000000000..df86b02d1d --- /dev/null +++ b/.changeset/tidy-guests-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +export `createConfigSecretEnumerator` from `@backstage/backend-common` instead of `@backstage/backend-app-api`. 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-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-moles-appear.md b/.changeset/warm-moles-appear.md new file mode 100644 index 0000000000..c2809e606f --- /dev/null +++ b/.changeset/warm-moles-appear.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Fix GitHub `repository` event support. + +- `$.repository.organization` is only provided for `push` events. Switched to `$.organization.login` instead. +- `$.repository.url` is not always returning the expected and required value. Use `$.repository.html_url` instead. 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/weak-jobs-joke.md b/.changeset/weak-jobs-joke.md new file mode 100644 index 0000000000..8127605f07 --- /dev/null +++ b/.changeset/weak-jobs-joke.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': patch +--- + +Add `tableOptions` to all tables and additionally `title` to API tables. 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..320bb2e0ce 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -243,8 +243,10 @@ Mkdocs monorepo Monorepo monorepos +morgan 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/beps/0010-scaffolder-templating-in-parameters/README.md b/beps/0010-scaffolder-templating-in-parameters/README.md new file mode 100644 index 0000000000..354dc66ea9 --- /dev/null +++ b/beps/0010-scaffolder-templating-in-parameters/README.md @@ -0,0 +1,223 @@ +--- +title: Supporting templating syntax in `parameters` schema +status: provisional +authors: + - '@benjdlambert' +owners: + - '@benjdlambert' + - '@backstage/scaffolder-maintainers' +project-areas: + - scaffolder +creation-date: 2024-03-26 +--- + + + +# BEP: Supporting templating syntax in `parameters` schema + + + +[**Discussion Issue**](https://github.com/backstage/backstage/issues/16275) + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) +- [Design Details](#design-details) +- [Release Plan](#release-plan) +- [Dependencies](#dependencies) +- [Alternatives](#alternatives) + +## Summary + + + +This BEP proposes to add support for templating syntax in the `parameters` schema of a scaffolder template. +This will allow users to define properties in the JSON Schema which are templated from current values that have been collected from the user already. +This can be useful when you want to use a value that has already been collected as a default value in another field. + +For example: + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: my-template +spec: + parameters: + - title: Some input + description: Get some info from the user + properties: + name: + type: string + default: Test + description: + type: string + default: ${{ parameters.name or "unknown" }}-description +``` + +## Motivation + + + +Inclusive of the initial RFC there's been a swarm of issues that are requesting this feature, and we want to align on the implementation and design of this feature. + +See the following: + +- https://github.com/backstage/backstage/issues/16275 +- https://github.com/backstage/backstage/pull/23283 +- https://github.com/backstage/backstage/issues/19597 +- https://github.com/backstage/backstage/issues/20533 +- https://github.com/backstage/backstage/pull/17746 + +There's some ideas for introducing a templating syntax for both templating into the `parameters` schema, and also being able to pass through some templating strings to underlying field extensions that can use those templating strings. +We want to align here so that we're not going to have those conflict or compete, and create a standard for how to achieve templating in both circumstances. + +### Goals + + + +- This BEP will settle the implementation for the templating of fields into the JSON Schema in the `parameters` section in the scaffolder templates. +- This BEP will settle how to pass through templating strings to underlying field extensions in a non-conflicting way. + +### Non-Goals + + + +## Proposal + +The proposal is to be able to decorate the template schema server side with a context and use that to drive the form rendering client side. + +We can extend the `/parameter-schema` endpoint to accept a `formData` context query parameter which will be a JSON object of the current `formData` state. This in turn allows the scaffolder frontend to repeatedly call the endpoint to get the updated rendered parameter schema. We'll need to turn the endpoint into a `POST` endpoint to accept the form data, but will retain the `GET` version for backwards compatibility. + +## Design Details + +### Example implementation of the `/parameter-schema` endpoint + +```diff +export interface ScaffolderApi { + getTemplateParameterSchema( + templateRef: string, ++ formData?: JsonObject, + ): Promise; +} +``` + +```diff + router +- .get( ++ .post( + '/v2/templates/:namespace/:kind/:name/parameter-schema', + async (req, res) => { + const credentials = await httpAuth.credentials(req); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: credentials, + targetPluginId: 'catalog', + }); + const template = await authorizeTemplate( + req.params, + token, + credentials, + ); + + const parameters = [template.spec.parameters ?? []].flat(); ++ const secureTemplater = await SecureTemplater.loadRenderer({ ++ templateFilters: { ++ ...createDefaultFilters({ integrations }), ++ ...additionalTemplateFilters, ++ }, ++ templateGlobals: additionalTemplateGlobals, ++ }); ++ ++ const templatedParameters = parameters.map(parameter => ++ renderTemplateString( ++ parameter, ++ { ++ parameters: req.body.formData, ++ }, ++ secureTemplater, ++ logger, ++ ), ++ ); +``` + +You can see a quick implementation of this in this [branch](https://github.com/backstage/backstage/compare/master...blam/templating-in-parameters) + +### Workaround for the `default` field + +There's a slight issue with the implementation of the `react-jsonschema-form`, which makes things like live updating on things like the `default` field slightly more difficult. +Currently, on first render, the default value is populated and then stored in the `formData` object or the current state, and the default value is never re-evaluated again at a later stage. + +This means that if end users are wanting to set default values with `${{ parameters.myOtherProperty }}`, then they would need to ensure that they are on different steps in the form +as the form would need to be re-rendered, and for performance reasons, we don't want to re-render the form on every `formData` update. + +We could fix this, by implementing custom logic for when the `parameter-schema` is updated, if the updated field is in a `default: *` field, then we replace the previous value with the new value in the `formData` automatically. +This is a pretty ugly workaround, but maybe the only option we have. Also at this point, pretty unsure if this affects any other parts of the `JSONSchema`, and we would also have to implement it for those fields if they exist. + +### Templated error messages + +Templating for `errorMessages` has been solved by using the `ajv-errors` library https://github.com/backstage/backstage/pull/25624, you can see more about [`backrefs` and pointers here](https://ajv.js.org/packages/ajv-errors.html). Any other template strings that will be passed through the underlying components and to be left untemplated should be encapsulated with options instead of passing through raw strings. The below example illustrates an `entityAndName` format, which under the hood, might do something like `${{ parameters.entity }} - ${{ parameters.name }}`, but this implementation never leaks out to the templating language. + +```yaml +parameters: + properties: + ... + description: + type: string + default: Test-description + ui:field: CustomDisplayField + ui:options: + format: entityAndName +``` + +## Release Plan + + + +This change is backwards compatible, and can be released in a minor release. There's no breaking changes to worry about here. + +## Dependencies + + + +## Alternatives + + + +### Templating client side + +- This could lead to confusion as `filters` such as `parseRepoUrl` and `pick` and any custom filters which you define in the backend would not be available in the client side. + +- Also with the limitations of the `default` value being updated only on first render and never re-evaluated, there's no performance benefit of doing things client side anymore. + +### Accept limitation of the `default` field + +Rather than using a workaround to support re-evaluating the `default` field, we could instead accept it as a limitation, and document it as such. + +This is not desirable, as it is likely a very common use-case to want to template the `default` field, leading to a poor template creation experience. diff --git a/contrib/docker/minimal-hardened-image/Dockerfile b/contrib/docker/minimal-hardened-image/Dockerfile index 8ee6a3d206..a247e0dff1 100644 --- a/contrib/docker/minimal-hardened-image/Dockerfile +++ b/contrib/docker/minimal-hardened-image/Dockerfile @@ -9,53 +9,79 @@ # # Once the commands have been run, you can build the image using `yarn docker-build` -FROM cgr.dev/chainguard/wolfi-base:latest +# syntax = docker/dockerfile:1.4 -ENV NODE_VERSION 18=~18.19 -ENV PYTHON_VERSION 3.12=~3.12 +# Build Python environment in a separate builder stage +FROM cgr.dev/chainguard/python:latest-dev as python-builder -RUN apk add nodejs-$NODE_VERSION yarn +ENV PATH=/venv/bin:$PATH -# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, -# in which case you should also move better-sqlite3 to "devDependencies" in package.json. -# Additionally, we install dependencies for `techdocs.generator.runIn: local`. -# https://backstage.io/docs/features/techdocs/getting-started#disabling-docker-in-docker-situation-optional -RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \ - --mount=type=cache,target=/var/lib/apk,sharing=locked \ - apk update && \ - apk add sqlite-dev python-$PYTHON_VERSION py3-pip python-3-dev py3-setuptools build-base gcc libffi-dev glibc-dev openssl-dev brotli-dev c-ares-dev nghttp2-dev icu-dev zlib-dev gcc-12 libuv-dev && \ - yarn config set python /usr/bin/python3 +RUN --mount=type=cache,target=/home/nonroot/.cache/pip,uid=65532,gid=65532 \ + python3 -m venv /home/nonroot/venv && \ + /home/nonroot/venv/bin/pip install mkdocs-techdocs-core==1.3.3 -# Set up a virtual environment for mkdocs-techdocs-core. -ENV VIRTUAL_ENV=/opt/venv -RUN python3 -m venv $VIRTUAL_ENV -ENV PATH="$VIRTUAL_ENV/bin:$PATH" +# Build Node environment in a separate builder stage +FROM cgr.dev/chainguard/wolfi-base:latest as node-builder -RUN pip3 install mkdocs-techdocs-core==1.3.3 - -# From here on we use the least-privileged `node` user to run the backend. -WORKDIR /app -RUN chown nonroot:nonroot /app -USER nonroot - -# This switches many Node.js dependencies to production mode. +ENV NODE_VERSION 20=~20.11 ENV NODE_ENV production -# Copy over Yarn 3 configuration, release, and plugins -COPY --chown=nonroot:nonroot .yarn ./.yarn -COPY --chown=nonroot:nonroot .yarnrc.yml ./ +RUN --mount=type=cache,target=/var/cache/apk,sharing=locked,uid=65532,gid=65532 \ + --mount=type=cache,target=/var/lib/apk,sharing=locked,uid=65532,gid=65532 \ + apk update && \ + apk add nodejs-$NODE_VERSION yarn \ + # Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend. + openssl-dev brotli-dev c-ares-dev nghttp2-dev icu-dev zlib-dev gcc-12 libuv-dev build-base -# Copy repo skeleton first, to avoid unnecessary docker cache invalidation. -# The skeleton contains the package.json of each package in the monorepo, -# and along with yarn.lock and the root package.json, that's enough to run yarn install. -COPY --chown=nonroot:nonroot yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ +WORKDIR /app +RUN chown -R nonroot:nonroot /app + +RUN mkdir -p /home/nonroot/.yarn/berry && chown -R 65532:65532 /home/nonroot/.yarn/berry + +USER nonroot + +COPY --chown=65532:65532 .yarn ./.yarn +COPY --chown=65532:65532 .yarnrc.yml ./ + +COPY --chown=65532:65532 yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz -RUN --mount=type=cache,target=/home/node/.yarn/berry/cache,sharing=locked,uid=1000,gid=1000 \ - yarn workspaces focus --all --production +RUN --mount=type=cache,target=/home/nonroot/.yarn/berry/cache,sharing=locked,uid=65532,gid=65532 \ + yarn workspaces focus --all --production && yarn cache clean --all -# Then copy the rest of the backend bundle, along with any other files we might want. -COPY --chown=nonroot:nonroot packages/backend/dist/bundle.tar.gz app-config*.yaml ./ -RUN tar xzf bundle.tar.gz && rm bundle.tar.gz +# Final stage to build the application image +FROM cgr.dev/chainguard/wolfi-base:latest +ENV PYTHON_VERSION 3.12=~3.12 +ENV NODE_VERSION 20=~20.14 +ENV NODE_ENV production + +RUN --mount=type=cache,target=/var/cache/apk,sharing=locked,uid=65532,gid=65532 \ + --mount=type=cache,target=/var/lib/apk,sharing=locked,uid=65532,gid=65532 \ + apk update && \ + apk add \ + # add node for backstage + nodejs-$NODE_VERSION \ + # add python for backstage techdocs + python-$PYTHON_VERSION \ + # add tini for init process + tini + +WORKDIR /app + +COPY package.json app-config.yaml ./ +ADD packages/backend/dist/skeleton.tar.gz packages/backend/dist/bundle.tar.gz ./ + +RUN chown -R 65532:65532 /app +RUN chown -R 65532:65532 /tmp +USER 65532:65532 + +COPY --from=node-builder --chown=65532:65532 /app/node_modules ./node_modules +COPY --from=python-builder --chown=65532:65532 /home/nonroot/venv /home/nonroot/venv +ENV PATH=/home/nonroot/venv/bin:$PATH + +ENV NODE_OPTIONS="--no-node-snapshot" +ENV GIT_PYTHON_REFRESH="quiet" + +ENTRYPOINT ["tini", "--"] CMD ["node", "packages/backend", "--config", "app-config.yaml"] diff --git a/contrib/docker/minimal-hardened-image/README.md b/contrib/docker/minimal-hardened-image/README.md index 62045a4ded..191c77986c 100644 --- a/contrib/docker/minimal-hardened-image/README.md +++ b/contrib/docker/minimal-hardened-image/README.md @@ -4,6 +4,19 @@ DockerHub images in general did not seem ideal for Backstage as the number of vu The `Dockerfile` in this directory uses a [wolfi-base](https://github.com/wolfi-dev) image from Chainguard Images. This improves the security of the application and reduces false positives in scanners. +## Steps taken + +When converting, I utilized the upstream Dockerfile as a starting point. + +- Multi-stage build - The Dockerfile has been split up into a multistage build which reduces the files, packages, executables, and directories in the final image. + - Size savings = ~900mb + - Reduced attack surface +- Base Image - Swap to [wolfi-base](https://github.com/wolfi-dev) image from Chainguard Images + - Vulnerability Savings = ~239 at the time of updating this README +- Entrypoint - Swap from `node` to `tini` as entrypoint to ensure that the default signal handlers work and zombie processes are handled properly +- Use `ADD` instead of `COPY` in dockerfile to reduce copied compressed files + - When a `rm` is used to remove a compressed file it still makes its way into the final image. Using `ADD` is safe with local files. + ## Pinning Digest To reduce maintenance, the digest of the image has been removed from the `./Dockerfile` file. A complete example with the digest would be `cgr.dev/chainguard/wolfi-base:latest@sha256:3d6dece13cdb5546cd03b20e14f9af354bc1a56ab5a7b47dca3e6c1557211fcf` and it is suggested to update the `FROM` line in the `Dockerfile` to use a digest. diff --git a/contrib/docs/tutorials/prometheus-metrics.md b/contrib/docs/tutorials/prometheus-metrics.md index fb4366c7f7..0db97c649e 100644 --- a/contrib/docs/tutorials/prometheus-metrics.md +++ b/contrib/docs/tutorials/prometheus-metrics.md @@ -1,5 +1,8 @@ # Prometheus +> [!NOTE] +> The Prometheus metrics have been marked as deprecated and will be removed at a later point. The recommendation is to use the OpenTelemetry metrics by following the [Setup OpenTelemetry](https://backstage.io/docs/tutorials/setup-opentelemetry) documentation + ## Overview This is a small tutorial that goes over how to setup your Backstage instance to output metrics in a format that can be pulled in by Prometheus. diff --git a/docs/auth/cloudflare/provider.md b/docs/auth/cloudflare/provider.md index 516ea9d647..a1946580a7 100644 --- a/docs/auth/cloudflare/provider.md +++ b/docs/auth/cloudflare/provider.md @@ -32,6 +32,12 @@ auth: serviceTokens: - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' subject: 'bot-user@your-company.com' + # You can customize the header name that contains the jwt token, by default + # cf-access-jwt-assertion is used + jwtHeaderName: + # You can customize the authorization cookie name, by default + # CF_Authorization is used + authorizationCookieName: # This picks what sign in resolver(s) you want to use. signIn: resolvers: diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index 6479ac7268..298f212399 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -24,7 +24,7 @@ Let's start by adding the following `auth` configuration in your ```yaml auth: providers: - gcp-iap: + gcpIap: audience: '/projects//global/backendServices/' jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT signIn: @@ -77,6 +77,6 @@ backend.add(import('@backstage/plugin-auth-backend-module-gcp-iap-provider')); ## Adding the provider to the Backstage frontend -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `gcp-iap` as the provider name. +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `gcpIap` as the provider name. If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. 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/auth/index.md b/docs/auth/index.md index 5188d7e2a4..de579d039a 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -112,9 +112,16 @@ const app = createApp({ }); ``` -You can also use the `providers` prop to enable multiple sign-in methods, for example +:::note Note -- allowing guest access: +You can configure sign-in to use a redirect flow with no pop-up by adding +`enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` + +::: + +### Using Multiple Providers + +You can also use the `providers` prop to enable multiple sign-in methods, for example to allow guest access: ```tsx title="packages/app/src/App.tsx" const app = createApp({ @@ -140,12 +147,53 @@ const app = createApp({ }); ``` -:::note Note +### Conditionally Render Sign In Provider -You can configure sign-in to use a redirect flow with no pop-up by adding -`enableExperimentalRedirectFlow: true` to the root of your `app-config.yaml` +In the above example you have both Guest and GitHub sign-in options, this is helpful for non-production but in Production you will most likely not want to offer Guest access. You can easily use information from your config to help conditionally render the provider: -::: +```tsx title="packages/app/src/App.tsx" +import { + configApiRef, + githubAuthApiRef, + useApi, +} from '@backstage/core-plugin-api'; + +const app = createApp({ + components: { + SignInPage: props => { + const configApi = useApi(configApiRef); + if (configApi.getString('auth.environment') === 'development') { + return ( + + ); + } + return ( + + ); + }, + }, + // .. +}); +``` ## Sign-In with Proxy Providers 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-http-router.md b/docs/backend-system/core-services/root-http-router.md index b3522e1752..88a10455f5 100644 --- a/docs/backend-system/core-services/root-http-router.md +++ b/docs/backend-system/core-services/root-http-router.md @@ -52,19 +52,49 @@ You can configure the root HTTP Router service by passing the options to the `cr ```ts import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; +import { RequestHandler } from 'express'; +import morgan from 'morgan'; const backend = createBackend(); backend.add( rootHttpRouterServiceFactory({ configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + // Refer to https://expressjs.com/en/guide/writing-middleware.html on how to write express middleware + const customMiddleware = { + logging(): RequestHandler { + const middlewareLogger = logger.child({ + type: 'incomingRequest', + }); + return (req, res, next) => { + // Custom Logging Implementation + next(); + }; + }, + // Default logging middleware uses the [morgan](https://github.com/expressjs/morgan) middleware which you can configure with custom formats. + morganLogging(): RequestHandler { + const middlewareLogger = logger.child({ + type: 'incomingRequest', + }); + const customMorganFormat = + '[:date[clf]] ":method :url HTTP/:http-version" :status ":user-agent"'; + return morgan(customMorganFormat, { + stream: { + write(message: string) { + logger.info(message.trimEnd()); + }, + }, + }); + }, + }; + // the built in middleware is provided through an option in the configure function app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); // you can add you your own middleware in here - app.use(custom.logging()); + app.use(customMiddleware.logging()); // here the routes that are registered by other plugins app.use(routes); diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index d47cb248f8..897498d28f 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -246,6 +246,9 @@ WORKDIR /app # Copy the install dependencies from the build stage and context COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton/ ./ +# Note: The skeleton bundle only includes package.json files -- if your app has +# plugins that define a `bin` export, the bin files need to be copied as well to +# be linked in node_modules/.bin during yarn install. RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ yarn install --frozen-lockfile --production --network-timeout 600000 diff --git a/docs/features/search/collators.md b/docs/features/search/collators.md new file mode 100644 index 0000000000..4054ec1212 --- /dev/null +++ b/docs/features/search/collators.md @@ -0,0 +1,107 @@ +--- +id: collators +title: Collators +description: Indexing you Backstage content with Collators +--- + +Backstage includes 2 [collators](./concepts.md#collators) out of the box for the [Catalog](#catalog) and [TechDocs](#techdocs). There's also some from the [Backstage Community](#community-collators) too! + +## Catalog + +The Catalog collator will index all the Entities in your Catalog. It is installed by default but if you need to add it manually here's how. + +First we add the plugin into your backend app: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-catalog +``` + +Then add the following line: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); + +// Other plugins... + +// search plugin +backend.add(import('@backstage/plugin-search-backend/alpha')); + +/* highlight-add-start */ +backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); +/* highlight-add-end */ + +backend.start(); +``` + +### Configuring the Catalog Collator + +The default schedule for the Catalog Collator is to run every 10 minutes, you can provide your own schedule by adding it to your config: + +```yaml title="app-config.yaml +search: + collators: + catalog: + schedule: # same options as in TaskScheduleDefinition + # supports cron, ISO duration, "human duration" as used in code + initialDelay: { seconds: 90 } + # supports cron, ISO duration, "human duration" as used in code + frequency: { hours: 6 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } +``` + +## TechDocs + +The TechDocs collator will index all the TechDocs in your Catalog. It is installed by default but if you need to add it manually here's how. + +First we add the plugin into your backend app: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-techdocs +``` + +Then add the following line: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); + +// Other plugins... + +// search plugin +backend.add(import('@backstage/plugin-search-backend/alpha')); + +/* highlight-add-start */ +backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); +/* highlight-add-end */ + +backend.start(); +``` + +### Configuring the TechDocs Collator + +The default schedule for the TechDocs Collator is to run every 10 minutes, you can provide your own schedule by adding it to your config: + +```yaml title="app-config.yaml +search: + collators: + techdocs: + schedule: # same options as in TaskScheduleDefinition + # supports cron, ISO duration, "human duration" as used in code + initialDelay: { seconds: 90 } + # supports cron, ISO duration, "human duration" as used in code + frequency: { hours: 6 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } +``` + +## Community Collators + +Here are some of the known Search Collators available in from the Backstage Community: + +- [`@backstage/plugin-search-backend-module-explore`](https://github.com/backstage/backstage/tree/master/plugins/search-backend-module-explore): will index content from the [Explore plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/explore/plugins/explore). +- [`@backstage/plugin-search-backend-module-stack-overflow-collator`](https://github.com/backstage/backstage/tree/master/plugins/search-backend-module-stack-overflow-collator): will index content from Stack Overflow. +- [`@backstage-community/search-backend-module-adr`](https://github.com/backstage/community-plugins/tree/main/workspaces/adr/plugins/search-backend-module-adr): will index content from the [ADR plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/adr/plugins/adr). + +## Custom Collators + +To create your own collators/decorators modules, please use the [searchModuleCatalogCollator](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-catalog/src/alpha.ts#L49) as an example, we recommend that modules are separated by plugin packages (e.g. `search-backend-module-`). You can also find the available search engines and collator/decorator modules documentation in the Alpha API reports. diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index b2a074974f..c2ca2e1dce 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -133,79 +133,34 @@ For more information about using `Root.tsx`, please see Add the following plugins into your backend app: ```bash title="From your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-node +yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-module-pg @backstage/plugin-search-backend-module-catalog @backstage/plugin-search-backend-module-techdocs ``` -Create a `packages/backend/src/plugins/search.ts` file containing the following -code: +Then add the following lines: -```typescript -import { useHotCleanup } from '@backstage/backend-common'; -import { createRouter } from '@backstage/plugin-search-backend'; -import { - IndexBuilder, - LunrSearchEngine, -} from '@backstage/plugin-search-backend-node'; -import { PluginEnvironment } from '../types'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; -import { Router } from 'express'; +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const searchEngine = new LunrSearchEngine({ - logger: env.logger, - }); - const indexBuilder = new IndexBuilder({ - logger: env.logger, - searchEngine, - }); +// Other plugins... - const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({ - frequency: { minutes: 10 }, - timeout: { minutes: 15 }, - initialDelay: { seconds: 3 }, - }); +/* highlight-add-start */ +// search plugin +backend.add(import('@backstage/plugin-search-backend/alpha')); - indexBuilder.addCollator({ - schedule: every10MinutesSchedule, - factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { - discovery: env.discovery, - tokenManager: env.tokenManager, - }), - }); +// search engines +backend.add(import('@backstage/plugin-search-backend-module-pg/alpha')); - const { scheduler } = await indexBuilder.build(); +// search collators +backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); +backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); +/* highlight-add-end */ - scheduler.start(); - useHotCleanup(module, () => scheduler.stop()); - - return await createRouter({ - engine: indexBuilder.getSearchEngine(), - logger: env.logger, - }); -} +backend.start(); ``` -Make the following modifications to your `packages/backend/src/index.ts` file: +With the above setup Search will use the [Lunr](https://github.com/olivernn/lunr.js) in-memory Search Engine but if your have Postgres setup as your database then it will use Postgres as your Search Engine. Learn more in the [Search Engines](./search-engines.md) documentation. -Import the `plugins/search` file you created above: - -```typescript -import search from './plugins/search'; -``` - -Set up an environment for search: - -```typescript -const searchEnv = useHotMemoize(module, () => createEnv('search')); -``` - -Register the search service with the router: - -```typescript -apiRouter.use('/search', await search(searchEnv)); -``` +The above also sets up two Collators for you - Catalog and TechDocs - which will index content from these two locations so that you can easily search them. Learn more in the [Collators documentation](./collators.md). ## Customizing Search diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 9ce30da556..b0df9440f3 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -45,75 +45,6 @@ to do that in two steps. }); ``` -## How to index TechDocs documents - -The TechDocs plugin has supported integrations to Search, meaning that it -provides a default collator factory ready to be used. - -The purpose of this guide is to walk you through how to register the -[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/1adc2c7/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts) -in your App, so that you can get TechDocs documents indexed. - -If you have been through the -[Getting Started with Search guide](https://backstage.io/docs/features/search/getting-started), -you should have the `packages/backend/src/plugins/search.ts` file available. If -so, you can go ahead and follow this guide - if not, start by going through the -getting started guide. - -1. Import the `DefaultTechDocsCollatorFactory` from - `@backstage/plugin-search-backend-module-techdocs`. - - ```typescript - import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs'; - ``` - -2. If there isn't an existing schedule you'd like to run the collator on, be - sure to create it first. Something like... - - ```typescript - import { Duration } from 'luxon'; - - const every10MinutesSchedule = env.scheduler.createScheduledTaskRunner({ - frequency: Duration.fromObject({ seconds: 600 }), - timeout: Duration.fromObject({ seconds: 900 }), - initialDelay: Duration.fromObject({ seconds: 3 }), - }); - ``` - -3. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder. - - ```typescript - indexBuilder.addCollator({ - schedule: every10MinutesSchedule, - factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { - discovery: env.discovery, - logger: env.logger, - tokenManager: env.tokenManager, - }), - }); - ``` - -You should now have your TechDocs documents indexed to your search engine of -choice! - -If you want your users to be able to filter down to the techdocs type when -searching, you can update your `SearchPage.tsx` file in -`packages/app/src/components/search` by adding `techdocs` to the list of values -of the `SearchType` component. - -```tsx title="packages/app/src/components/search/SearchPage.tsx" - - - {/* ... */} - -``` - -> Check out the documentation around [integrating search into plugins](../../plugins/integrating-search-into-plugins.md#create-a-collator) for how to create your own collator. - ## How to customize fields in the Software Catalog or TechDocs index Sometimes, you might want to have the ability to control which data passes into the search index @@ -388,38 +319,3 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => ( ``` There are other more specific search results layout components that also accept result item extensions, check their documentation: [SearchResultList](https://backstage.io/storybook/?path=/story/plugins-search-searchresultlist--with-result-item-extensions) and [SearchResultGroup](https://backstage.io/storybook/?path=/story/plugins-search-searchresultgroup--with-result-item-extensions). - -## How to migrate your backend installation to use Search together with the new backend system - -Recently, the Backstage maintainers [announced the new Backend System](https://backstage.io/blog/2023/02/15/backend-system-alpha). The search plugins are now migrated to support the new backend system. In this guide you will learn how to update your backend set up. - -In "packages/backend/index.ts", install the search plugin [1], the search engine [2], and the search collators/decorators modules [3]: - -```ts -const backend = createBackend(); -// [1] adding the search plugin to the backend -backend.add(import('@backstage/plugin-search-backend/alpha')); -// [2] (optional) the default search engine is Lunr, if you want to extend the search backend with another search engine. -backend.add( - import('@backstage/plugin-search-backend-module-elasticsearch/alpha'), -); -// [3] extending search with collator modules to start index documents, take in optional schedule parameters. -backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); - -backend.start(); -``` - -To create your own collators/decorators modules, please use the [searchModuleCatalogCollator](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-catalog/src/alpha.ts#L49) as an example, we recommend that modules are separated by plugin packages (e.g. `search-backend-module-`). You can also find the available search engines and collator/decorator modules documentation in the Alpha API reports: - -**Search engine modules** - -- Postgres [module](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-pg/alpha-api-report.md); -- Elasticsearch [module](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-elasticsearch/alpha-api-report.md). - -**Search collator/decorator modules** - -- Catalog [module](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-catalog/alpha-api-report.md); -- Explore [module](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-explore/alpha-api-report.md); -- TechDocs [module](https://github.com/backstage/backstage/blob/d7f955f300893f50c4882ea8f5c09aa42dfaacfd/plugins/search-backend-module-techdocs/alpha-api-report.md). diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index 86b6244fb6..f768a9bde8 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -4,34 +4,31 @@ title: Search Engines description: Choosing and configuring your search engine for Backstage --- -Backstage supports 3 search engines by default, an in-memory engine called Lunr, -Elasticsearch and Postgres. You can configure your own search engines by -implementing the provided interface as mentioned in the -[search backend documentation.](./getting-started.md#Backend) - -Provided search engine implementations have their own way of constructing -queries, which may be something you want to modify. Alterations to the querying -logic of a search engine can be made by providing your own implementation of a -QueryTranslator interface. This modification can be done without touching -provided search engines by using the exposed setter to set the modified query -translator into the instance. - -```typescript -const searchEngine = new LunrSearchEngine({ logger: env.logger }); -searchEngine.setTranslator(new MyNewAndBetterQueryTranslator()); -``` +Backstage supports 3 search engines by default, an in-memory engine called [Lunr](#lunr), [Postgres](#postgres) +and [Elasticsearch](#elasticsearch). ## Lunr -Lunr search engine is enabled by default for your backstage instance if you have -not done additional changes to the scaffolded app. +Lunr search engine is enabled by default for your Backstage instance if you have not done additional changes to the scaffolded app. -Lunr can be instantiated like this: +As Lunr is built into the Search backend plugin it can be added like this: -```typescript -// app/backend/src/plugins/search.ts -const searchEngine = new LunrSearchEngine({ logger: env.logger }); -const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine }); +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-search-backend +``` + +Then add the following line: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); + +// Other plugins... + +/* highlight-add-start */ +backend.add(import('@backstage/plugin-search-backend/alpha')); +/* highlight-add-end */ + +backend.start(); ``` :::note Note @@ -45,34 +42,39 @@ other search engines instead. ## Postgres -The Postgres based search engine only requires that postgres being configured as +The Postgres based search engine only requires that Postgres being configured as the database engine for Backstage. Therefore it targets setups that want to avoid maintaining another external service like Elasticsearch. The search provides decent results and performs well with ten thousands of indexed -documents. The connection to postgres is established via the database manager +documents. The connection to Postgres is established via the database manager also used by other plugins. > **Important**: The search plugin requires at least Postgres 12! -To use the `PgSearchEngine`, make sure that you have a Postgres database -configured and make the following changes to your backend: +First we need to add the plugin: -1. Add a dependency on `@backstage/plugin-search-backend-module-pg` to your - backend's `package.json`. -2. Initialize the search engine. It is recommended to initialize it with a - fallback to the lunr search engine if you are running Backstage for - development locally with SQLite: - -```typescript -// In packages/backend/src/plugins/search.ts - -// Initialize a connection to a search engine. -const searchEngine = (await PgSearchEngine.supported(env.database)) - ? await PgSearchEngine.fromConfig(env.config, { database: env.database }) - : new LunrSearchEngine({ logger: env.logger }); +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-pg ``` -## Optional Configuration +Then add the following line: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); + +// Other plugins... + +// search plugin +backend.add(import('@backstage/plugin-search-backend/alpha')); + +/* highlight-add-start */ +backend.add(import('@backstage/plugin-search-backend-module-pg/alpha')); +/* highlight-add-end */ + +backend.start(); +``` + +### Optional Configuration The following is an example of the optional configuration that can be applied when using Postgres as the search backend. Currently this is mostly for just the highlight feature: @@ -106,19 +108,32 @@ Backstage supports Elasticsearch (and OpenSearch) search engine connections, indexing and querying out of the box. Available configuration options enable usage of either AWS or Elastic.co hosted solutions, or a custom self-hosted solution. -Similarly to Lunr above, Elasticsearch can be set up like this: +Similarly to Postgres above, Elasticsearch can be set up as follows. -```typescript -// app/backend/src/plugins/search.ts -const searchEngine = await ElasticSearchSearchEngine.fromConfig({ - logger: env.logger, - config: env.config, -}); -const indexBuilder = new IndexBuilder({ logger: env.logger, searchEngine }); +First we need to add the plugin: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-search-backend-module-elasticsearch ``` -For the engine to be available, your backend package needs a dependency on -package `@backstage/plugin-search-backend-module-elasticsearch`. +Then add the following line: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); + +// Other plugins... + +// search plugin +backend.add(import('@backstage/plugin-search-backend/alpha')); + +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-search-backend-module-elasticsearch/alpha'), +); +/* highlight-add-end */ + +backend.start(); +``` Elasticsearch needs some additional configuration before it is ready to use within your instance. The configuration options are documented in the @@ -126,67 +141,12 @@ within your instance. The configuration options are documented in the The underlying functionality uses either the official Elasticsearch client version 7.x (meaning that Elasticsearch version 7 is the only one confirmed to -be supported), or the OpenSearch client, when the `aws` or `opensearch `provider +be supported), or the OpenSearch client, when the `aws` or `opensearch` provider is configured. -Should you need to create your own bespoke search experiences that require more -than just a query translator (such as faceted search or Relay pagination), you -can access the configuration of the search engine in order to create new -Elasticsearch clients. The version of the client need not be the same as one -used internally by the Elasticsearch engine plugin. For example: +### Example configurations -```typescript -import { isOpenSearchCompatible } from '@backstage/plugin-search-backend-module-elasticsearch'; -import { Client as ElasticClient } from '@elastic/elasticsearch'; -import { Client as OpenSearchClient } from '@opensearch-project/opensearch'; - -// Return an Elasticsearch client -const esClient = searchEngine.newClient(options => { - if (!isOpenSearchCompatible(options)) { - return new ElasticClient(options); - } - - throw new Error('Incompatible options'); -}); - -// Return an OpenSearch client -const osClient = searchEngine.newClient(options => { - if (isOpenSearchCompatible(options)) { - return new OpenSearchClient(options); - } - - throw new Error('Incompatible options'); -}); -``` - -#### Set custom index template - -The Elasticsearch engine gives you the ability to set a custom index template if needed. - -> Index templates define settings, mappings, and aliases that can be applied automatically to new indices. - -```typescript -// app/backend/src/plugins/search.ts -const searchEngine = await ElasticSearchSearchEngine.initialize({ - logger: env.logger, - config: env.config, -}); - -searchEngine.setIndexTemplate({ - name: '', - body: { - index_patterns: [''], - template: { - mappings: {}, - settings: {}, - }, - }, -}); -``` - -## Example configurations - -### AWS +#### AWS Using AWS hosted Elasticsearch the only configuration option needed is the URL to the Elasticsearch service. The implementation assumes that environment @@ -201,7 +161,7 @@ search: node: https://my-backstage-search-asdfqwerty.eu-west-1.es.amazonaws.com ``` -### Elastic.co +#### Elastic.co Elastic Cloud hosted Elasticsearch uses a Cloud ID to determine the instance of hosted Elasticsearch to connect to. Additionally, username and password needs to @@ -218,7 +178,7 @@ search: password: changeme ``` -### OpenSearch +#### OpenSearch OpenSearch can be self hosted for example with the [official docker image](https://hub.docker.com/r/opensearchproject/opensearch). The configuration requires only the node and authentication. @@ -232,7 +192,7 @@ search: password: changeme ``` -### Others +#### Others Other Elasticsearch instances can be connected to by using standard Elasticsearch authentication methods and exposed URL, provided that the cluster @@ -242,8 +202,6 @@ username/password or an API key. For more information how to create an API key, see [Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html). -#### Configuration examples - ##### With username and password ```yaml @@ -273,8 +231,6 @@ you may get an error caused by limited `thread_pool` configuration. ( `429 Too M In this case you need to decrease the batch size to index the resources to prevent this kind of error. You can easily decrease or increase the batch size in your `app-config.yaml` using the `batchSize` option provided for Elasticsearch configuration. -#### Configuration example - **Set batch size to 100** ```yaml 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-catalog/system-model.md b/docs/features/software-catalog/system-model.md index ee7270a960..b93dde37d2 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -50,7 +50,7 @@ components need to be in a known machine-readable format so we can build further tooling and analysis on top. APIs have a visibility: they are either public (making them available for any -other component to consume), restricted (only available to an allowlisted set of +other component to consume), restricted (only available to an allowed set of consumers), or private (only available within their system). As public APIs are going to be the primary way interaction between components, Backstage supports documenting, indexing and searching all APIs so we can browse them as 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..05f184e90f 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' @@ -203,7 +203,7 @@ techdocs: # (Optional) The time (in milliseconds) that the TechDocs backend will wait # for a cache service to respond before continuing on as though the cached - # object was not found (e.g. when the cache sercice is unavailable). The + # object was not found (e.g. when the cache service is unavailable). The # default value is 1000 readTimeout: 500 ``` diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 3a7cfd97bb..59198e919b 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -841,3 +841,8 @@ metadata: annotations: backstage.io/techdocs-entity: system:default/example ``` + +## How to resolve broken links from moved or renamed pages in your documentation site + +TechDocs supports using the [mkdocs-redirects](https://github.com/mkdocs/mkdocs-redirects/tree/master) plugin to create a redirect map for any TechDocs site. This allows broken links from renamed or moved pages in your site to be redirected to their specified replacement. +TechDocs will notify the user that the page they are trying to access is no longer maintained. Then, they will be redirected. External site redirects are not supported. If an external redirect is provided, the user will instead be redirected to the index page of the documentation site. diff --git a/docs/frontend-system/architecture/01-index.md b/docs/frontend-system/architecture/00-index.md similarity index 100% rename from docs/frontend-system/architecture/01-index.md rename to docs/frontend-system/architecture/00-index.md diff --git a/docs/frontend-system/architecture/04-plugins.md b/docs/frontend-system/architecture/04-plugins.md deleted file mode 100644 index 042d4fbc36..0000000000 --- a/docs/frontend-system/architecture/04-plugins.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: plugins -title: Frontend Plugins -sidebar_label: Plugins -# prettier-ignore -description: Frontend plugins ---- - -> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** - -## Introduction - - - -## Creating a Plugin - - - -```ts -export const myPlugin = createPlugin({ - id: 'my-plugin', -}); -``` - - - -### Plugin ID - - - -### Plugin Extensions - - - -### Plugin Routes - - - -### Plugin External Routes - - - -### Plugin Feature Flags - - - -## Installing a Plugin in an App - - diff --git a/docs/frontend-system/architecture/02-app.md b/docs/frontend-system/architecture/10-app.md similarity index 95% rename from docs/frontend-system/architecture/02-app.md rename to docs/frontend-system/architecture/10-app.md index 3b839d4232..532971d124 100644 --- a/docs/frontend-system/architecture/02-app.md +++ b/docs/frontend-system/architecture/10-app.md @@ -33,13 +33,13 @@ const rootEl = document.getElementById('root')!; ReactDOM.createRoot(rootEl).render(app); ``` -We call `createApp` to create a new app instance, which is responsible for wiring together all of the features that we provide to the app. It also provides a set of built-in [Extensions](./03-extensions.md) that help build out the foundations of the app, as well as defaults for many other systems such as [Utility API](./06-utility-apis.md) implementations, components, icons, themes, and how to load configuration. No real work is done at the point of creating the app though, it's all deferred to the rendering of the element returned from `app.createRoot()`. +We call `createApp` to create a new app instance, which is responsible for wiring together all of the features that we provide to the app. It also provides a set of built-in [Extensions](./20-extensions.md) that help build out the foundations of the app, as well as defaults for many other systems such as [Utility API](./33-utility-apis.md) implementations, components, icons, themes, and how to load configuration. No real work is done at the point of creating the app though, it's all deferred to the rendering of the element returned from `app.createRoot()`. -It is possible to explicitly install features when creating the app, although typically these will instead be discovered automatically which we'll explore later on. Nevertheless these features are what build out the actual functionality of the app by providing [Extensions](./03-extensions.md). These extensions are wired together by the app into a tree structure known as the app extension tree. Each node in this tree receives data from its child nodes, and passes along data to its parent. The following diagram illustrates the shape of a small app extension tree. +It is possible to explicitly install features when creating the app, although typically these will instead be discovered automatically which we'll explore later on. Nevertheless these features are what build out the actual functionality of the app by providing [Extensions](./20-extensions.md). These extensions are wired together by the app into a tree structure known as the app extension tree. Each node in this tree receives data from its child nodes, and passes along data to its parent. The following diagram illustrates the shape of a small app extension tree. ![frontend system app structure diagram](../../assets/frontend-system/architecture-app.drawio.svg) -Each node in this tree is an extension with a parent node and children. The colored shapes represent extension data inputs and output, where each color is one unique type of data. You can see that there are both extensions that output data that is ignored by the parent, as well as extensions that accept inputs but do not have any children. There are a couple of different tools at your disposal when creating and extension that lets you define different requirements for your inputs and output, which we will cover in greater details in the [Extensions](./03-extensions.md) section. +Each node in this tree is an extension with a parent node and children. The colored shapes represent extension data inputs and output, where each color is one unique type of data. You can see that there are both extensions that output data that is ignored by the parent, as well as extensions that accept inputs but do not have any children. There are a couple of different tools at your disposal when creating and extension that lets you define different requirements for your inputs and output, which we will cover in greater details in the [Extensions](./20-extensions.md) section. A common type of data that is shared between extensions is React elements and components. These can in turn be rendered by each other in their own React components, which ends up forming a parallel tree of React components that is similar in shape to that of the app extension tree. At the top of the app extension tree is a built-in root extension that among other things outputs a React element. This element also ends up being the root of the parallel React tree, and is rendered by the React element returned by `app.createRoot()`. diff --git a/docs/frontend-system/architecture/15-plugins.md b/docs/frontend-system/architecture/15-plugins.md new file mode 100644 index 0000000000..a54573427d --- /dev/null +++ b/docs/frontend-system/architecture/15-plugins.md @@ -0,0 +1,91 @@ +--- +id: plugins +title: Frontend Plugins +sidebar_label: Plugins +# prettier-ignore +description: Frontend plugins +--- + +> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** + +## Introduction + +Frontend plugins are a foundational building block in Backstage and the frontend system. They are used to encapsulate and provide functionality for a Backstage app, such as new pages, navigational elements, and APIs; as well as extensions and features for other plugins, such as entity page cards and content for the Software Catalog, or result list items for the search plugin. + +Each plugin is typically shipped in a separate NPM package, whether that's a published package, or just in the local workspace. The plugin instance should always the `default` export of the package, either via the main entry-point or the `/alpha` sub-path export. Each plugin package is limited to exporting a single plugin instance. In a local workspace you could use a different structure if preferred, but this is considered a non-standard layout and should be avoided in published packages. + +## Creating a Plugin + +Frontend plugin instances are created with the `createFrontendPlugin` function, which is provided by the `@backstage/frontend-plugin-api` package. It takes a single options object that provides all of the necessary configuration for the plugin. In particular you will want to provide [extensions](./20-extensions.md) for your plugin, as that is the way that you can provide new functionality to the app. + +```tsx +// This creates a new extension, see "Extension Blueprints" documentation for more details +const myPage = PageBlueprint.make({ + params: { + defaultPath: '/my-page', + loader: () => import('./MyPage').then(m => ), + }, +}); + +export default createFrontendPlugin({ + id: 'my-plugin', + extensions: [myPage], +}); +``` + +### `pluginId` option + +Each plugin needs an ID, which is used to uniquely identify the plugin within an entire Backstage system. The ID does not have to be globally unique across all of the NPM ecosystem, although you generally want to strive for that. It is not possible to install multiple plugins with the same ID in a single Backstage app. + +The plugin ID should generally be part of the of the package name and use kebab-case. See both the [frontend naming patterns section](./50-naming-patterns.md), as well as the [package metadata section](../../tooling/package-metadata.md#name) for more information. + +### `extensions` option + +These are the [extensions](./20-extensions.md) that the plugin provides to the app. Note that you should not export any of these extensions separately from the plugin package, as they can already by accessed via the `getExtension` method of the plugin instance using the extension ID. + +The extensions that you provide to a plugin will have their `namespace` set to the plugin ID by default. For example, if you create an extension using the `PageBlueprint` without any particular naming options and install that via a plugin with the ID `my-plugin`, the final extension ID will be `page:my-plugin`. You can read more about how this works in the [extension structure documentation](./20-extensions.md#extension-structure). + +### `routes` and `externalRoutes` options + +These are the routes that the plugin exposes to the app. The `routes` option declares all of the target routes that your plugin provides, i.e. routes that other plugins link to. The `externalRoutes` option instead declares all the outgoing routes, i.e. routes that your plugins links to, which you can bind to the `routes` of other plugins. See the [routes documentation](./36-routes.md) for more information how to set up cross-plugin navigation. + +### `featureFlags` option + +This is a list of feature flag declarations that your plugin provides to the app. This makes sure that the feature flags are correctly registered and can be toggled in the app. To read a feature flag you can use the feature flags [Utility API](../architecture/33-utility-apis.md), accessible via `featureFlagsApiRef`. + +## Installing a Plugin in an App + +A plugin instance is considered a frontend feature and can be installed directly in any Backstage frontend app. See the [app documentation](./10-app.md) for more information about the different ways in which you can install new features in an app. + +## Overriding a Plugin + +A plugin might not always behave exactly the way you want. It could be that you want to remove particular extensions, decorate them a bit, replace them with your own, or simply add new ones. Regardless of your exact use-case, you can use the `plugin.withOverrides` method to create a new copy of the plugin with the desired changes. When doing so you can also access the original extensions provided by the plugin, and use the [extension override](./25-extension-overrides.md) API to make changes to individual extensions: + +```tsx +import plugin from '@backstage/plugin-catalog'; + +export default plugin.withOverrides({ + // These overrides are merged with the original extensions + extensions: [ + // Override the catalog nav item to use a custom icon + plugin.getExtension('nav-item:catalog').override({ + factory: origFactory => [ + NavItemBlueprint.dataRefs.target({ + ...origFactory().get(NavItemBlueprint.dataRefs.target), + icon: CustomCatalogIcon, + }), + ], + }), + // Override the catalog index page with a completely custom implementation + PageBlueprint.make({ + params: { + defaultPath: '/catalog', + routeRef: plugin.routes.catalogIndex, + loader: () => import('./CustomCatalogIndexPage').then(m => ), + }, + }), + ], +}); +``` + +You can keep the plugin override in your app package, but it can often be a good idea to separate it out into its own package, especially if the overrides are complex or you want distinct ownership of the override. For example, if you are overriding the `@backstage/plugin-catalog` plugin, you might create a new package called `@internal/plugin-catalog` at `plugins/catalog` in your workspace, which exports the overridden plugin instance. diff --git a/docs/frontend-system/architecture/03-extensions.md b/docs/frontend-system/architecture/20-extensions.md similarity index 66% rename from docs/frontend-system/architecture/03-extensions.md rename to docs/frontend-system/architecture/20-extensions.md index 2f3daba6f8..02f4ba9d05 100644 --- a/docs/frontend-system/architecture/03-extensions.md +++ b/docs/frontend-system/architecture/20-extensions.md @@ -8,7 +8,7 @@ description: Frontend extensions > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -As mentioned in the [previous section](./02-app.md), Backstage apps are built up from a tree of extensions. This section will go into more detail about what extensions are, how to create and use them, and how to create your own extensibility patterns. +As mentioned in the [previous section](./10-app.md), Backstage apps are built up from a tree of extensions. This section will go into more detail about what extensions are, how to create and use them, and how to create your own extensibility patterns. ## Extension Structure @@ -18,12 +18,12 @@ Each extensions has a number of different properties that define how it behaves ### ID - - The ID of an extension is used to uniquely identity it, and it should ideally be unique across the entire Backstage ecosystem. For each frontend app instance there can only be a single extension for any given ID. Installing multiple extensions with the same ID will either result in an error or one of the extensions will override the others. The ID is also used to reference the extensions from other extensions, in configuration, and in other places such as developer tools and analytics. +When creating an extension you do not provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the [extension blueprint](./23-extension-blueprints.md), the only exception is if you use [`createExtension`](#creating-an-extensions) directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. + +The extension ID will be constructed using the pattern `[:][][/][]`, where the separating `/` is only present if both a namespace and name are defined. + ### Output The output of an extension is the data that it provides to its parent extension, and ultimately its contribution to the app. The output itself comes in the form of a collection of arbitrary values, anything that can be represented as a TypeScript type. However, each individual output value must be associated with a shared reference known as an extension data reference. You must also use these same references to be able to access individual output values of an extension. @@ -32,7 +32,7 @@ The output of an extension is the data that it provides to its parent extension, The inputs of an extension define the data that it received from its children. Each extension can have multiple different inputs identified by an input name. These inputs each have their own set of data that they expect, which is defined as a collection of extension data references. An extension will only have access to the data that it has explicitly requested from each input. -### Attachment Point +### Attachment point The attachment point of an extension decides where in the app extension tree it will be located. It is defined by the ID of the parent extension, as well as the name of the input to attach to. Through the attachment point the extension will share its own output as inputs to the parent extension. An extension can only be attached to an input that matches its own output, it is an error to try to attach an extension to an input the requires data that the extension does not provide in its output. @@ -44,7 +44,7 @@ Each extension in the app can be disabled, meaning it will not be instantiated a The ordering of extensions is sometimes very important, as it may for example affect in which order they show up in the UI. When an extension is toggled from disabled to enabled through configuration it resets the ordering of the extension, pushing it to the end of the list. It is generally recommended to leave extensions as disabled by default if their order is important, allowing for the order in which their are enabled in the configuration to determine their order in the app. -### Configuration & Configuration Schema +### Configuration & configuration schema Each extension can define a configuration schema that describes the configuration that it accepts. This schema is used to validate the configuration provided by integrators, but also to fill in default configuration values. The configuration itself is provided by integrators in order to customize the extension. It is not possible to provide a default configuration of an extension, this must instead be done through defaults in the configuration schema. This allows for a simpler configuration logic where multiple configurations of the same extension completely replace each other rather than being merged. @@ -54,7 +54,7 @@ The extension factory is the implementation of the extension itself. It is a fun Extension factories should be lean and not do any heavy lifting or async work, as they are called during the initialization of the app. For example, if you need to do an expensive computation to generate your output, then prefer outputting a callback that does the computation instead. This allows the parent extension to defer the computation for later so that you avoid blocking the app startup. -## Creating an Extensions +## Creating an extension Extensions are created using the `createExtension` function from `@backstage/frontend-plugin-api`. At minimum you need to provide an ID, attachment point, output definition, and a factory function. The following example shows the creation of a minimal extension: @@ -64,28 +64,22 @@ const extension = createExtension({ // This is the attachment point, `id` is the ID of the parent extension, // while `input` is the name of the input to attach to. attachTo: { id: 'my-parent', input: 'content' }, - // The output map defines the outputs of the extension. The object keys - // are only used internally to map the outputs of the factory and do - // not need to match the keys of the input. - output: { - element: coreExtensionData.reactElement, - }, + // The output option defines the allowed and required outputs of the extension factory. + output: [coreExtensionData.reactElement], // This factory is called to instantiate the extensions and produce its output. factory() { - return { - element:
Hello World
, - }; + return [coreExtensionData.reactElement(
Hello World
)]; }, }); ``` -Note that while the `createExtension` is public API and used in many places, it is not typically what you use when building plugins and features. Instead there are many extension creator functions exported by both the core APIs and plugins that make it easier to create extensions for more specific usages. +Note that while the `createExtension` function is public API and used in many places, it is not typically what you use when building plugins and features. Instead there are many [extension blueprints](./23-extension-blueprints.md) exported by both the core APIs and plugins that make it easier to create extensions for more specific usages. -## Extension Data +## Extension data Communication between extensions happens in one direction, from one child extension through the attachment point to its parent. The child extension outputs data which is then passed as inputs to the parent extension. This data is called Extension Data, where the shape of each individual piece of data is described by an Extension Data Reference. These references are created separately from the extensions themselves, and can be shared across multiple different kinds of extensions. Each reference consists of an ID and a TypeScript type that the data needs to conform to, and represents one type of data that can be shared between extensions. -### Extension Data References +### Extension data references To create a new extension data reference to represent a type of shared extension data you use the `createExtensionDataRef` function. When defining a new reference you need to provide an ID and a TypeScript type, for example: @@ -101,59 +95,53 @@ The `ExtensionDataRef` can then be used to describe an output property of the ex ```tsx const extension = createExtension({ // ... - output: { - element: reactElementExtensionDataRef, - }, + output: [reactElementExtensionDataRef], factory() { - return { - element:
Hello World
, - }; + return [reactElementExtensionDataRef(
Hello World
)]; }, }); ``` -### Extension Data Uniqueness +### Extension data uniqueness -Note that the key used in the output map, in this case `element`, is only used internally within the definition of the extension itself. That actual identifier for the data when consumed by other extensions is the ID of the reference, in this case [`core.reactElement`](https://github.com/backstage/backstage/blob/916da47e8abdb880877daa18881eb8fdbb33e70a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts#L23). This means that you can not output multiple different values for the same extension data reference, as they would conflict with each other. That in turn makes overly generic extension data references a bad idea, for example a generic "string" type. Instead create separate references for each type of data that you want to share. +Note that you are **not** allowed to repeat the same data reference in the outputs, or return multiple values for the same reference. Multiple outputs for the same reference will conflict with each other and cause an error. If you want to output multiple values of the same TypeScript type you should create separate references for each value. That in turn means that overly generic extension data references are a bad idea, for example a generic "string" type. Instead create separate references for each type of data that you want to share. ```tsx const extension = createExtension({ // ... - output: { - // ❌ Bad example - outputting values of same type - element1: reactElementExtensionDataRef, - element2: reactElementExtensionDataRef, - }, + output: [ + // ❌ Bad example - duplicate output declaration + reactElementExtensionDataRef, + reactElementExtensionDataRef, + ], factory() { - return { - element1:
Hello World
, - element2:
Hello World
, - }; + return [ + // ❌ Bad example - duplicate output values + reactElementExtensionDataRef(
Hello
), + reactElementExtensionDataRef(
World
), + ]; }, }); ``` -### Core Extension Data +### Core extension data We provide default `coreExtensionData`, which provides commonly used `ExtensionDataRef`s - e.g. for `React.JSX.Element` and `RouteRef`. They can be used when creating your own extension. For example, the React Element extension data that we defined above is already provided as `coreExtensionData.reactElement`. For a full list and explanations of all types of core extension data, see the [core extension data reference](../building-plugins/04-built-in-data-refs.md). -### Optional Extension Data +### Optional extension data By default all extension data is required, meaning that the extension factory must provide a value for each output. However, it is possible to make extension data optional by calling the `.optional()` method. This makes it optional for the factory function to return a value as part of its output. When calling the `.optional()` method you create a new copy of the extension data reference, it does not mutate the existing reference. ```tsx const extension = createExtension({ // ... - output: { - element: coreExtensionData.reactElement.optional(), - }, + output: [coreExtensionData.reactElement.optional()], factory() { - return { - element: - Math.random() < 0.5 ? : undefined, - }; + return Math.random() > 0.5 + ? [coreExtensionData.reactElement(
Hello World
)] + : []; }, }); ``` @@ -167,17 +155,19 @@ const navigationExtension = createExtension({ // ... inputs: { // [1]: Input - logo: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true, optional: true }, - ), + logo: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + optional: true, + }), }, factory({ inputs }) { return { element: ( - + ), }; }, @@ -193,10 +183,9 @@ So how can we now attach the output to the parent extension's input? If we think const navigationItemExtension = createExtension({ // ... attachTo: { id: 'app/nav', input: 'items' }, + output: [coreExtensionData.reactElement], factory() { - return { - element: Home, - }; + return [coreExtensionData.reactElement(Home)]; }, }); @@ -205,26 +194,22 @@ const navigationExtension = createExtension({ // [2]: Extension `id` will be `app/nav` following the extension naming pattern namespace: 'app', name: 'nav', - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], inputs: { - items: createExtensionInput({ - element: coreExtensionData.reactElement, - }), + items: createExtensionInput([coreExtensionData.reactElement]), }, factory({ inputs }) { - return { - element: ( + return [ + coreExtensionData.reactElement( + , ), - }; + ]; }, // ... }); @@ -232,27 +217,27 @@ const navigationExtension = createExtension({ In this case the extension input `items` is an array, where each individual item is an extension that attached itself to the extension inputs of this `id`. -With the `inputs` not only the `output` of an extensions item is passed to the extension, but also the `node`. However, it is discouraged to consume the `node` here unless needed. If we are looking at the `factory` function from the example above we could access the `node` like the following: +In addition to being able to access data passed through the input, you also have access to the underlying app `node`. This can be useful if you for example want to get the ID of the attached extension. However, avoid using the `node` unless needed, it is generally better to stick to only consuming the provided data. If we are looking at the `factory` function from the example above we could access the `node` like the following: ```tsx // ... factory({ inputs }) { - return { - element: ( + return [ + coreExtensionData.reactElement( ), - }; + ]; }, ``` -## Extension Configuration +## Extension configuration With the `app-config.yaml` there is already the option to pass configuration to plugins or the app to e.g. define the `baseURL` of your app. For extensions this concept would be limiting as an extension can be independent of the plugin & initiated several times. Therefore we created a possibility to configure each extension individually through config. The extension config schema is created using the [`zod`](https://zod.dev/) library, which in addition to TypeScript type checking also provides runtime validation and coercion. If we continue with the example of the `navigationExtension` and now want it to contain a configurable title, we could make it available like the following: @@ -262,20 +247,20 @@ const navigationExtension = createExtension({ namespace: 'app', name: 'nav', // [3]: Extension `id` will be `app/nav` following the extension naming pattern - configSchema: createSchemaFromZod(z => - z.object({ - title: z.string().default('Sidebar Title'), - }), - ), + config: { + schema: { + title: z => z.string().default('Sidebar Title'), + }, + }, factory({ config }) { - return { - element: ( + return [ + coreExtensionData.reactElement( + , ), - }; + ]; }, // ... }); @@ -293,47 +278,40 @@ app: title: 'Backstage' ``` -## Extension Creators +## Extension factory as a generator function -With creating an extension by using `createExtension(...)` you have the advantage that the extension can be anything in your Backstage application. We realised that this comes with the trade-off of having to repeat boilerplate code for similar building blocks. Here extension creators come into play for covering common building blocks in Backstage like pages using `createPageExtension`, themes using the `createThemeExtension` or items for the navigation using `createNavItemExtension`. +In all examples so far we have defined the extension factory as a regular function that returns its output in an array. However, the only requirement is that the factory function returns any iterable of extension data values. This means that you can also define the factory function as a generator function, which allows you to yield values one by one. This is particularly useful if you want to conditionally output values. -If we follow the example from above all items of the navigation have similarities, like they all want to be attached to the same extension with the same input as well as rendering the same navigation item component. Therefore `createExtension` can be abstracted for this use case to `createNavItemExtension` and if we add the extension to the app it will end up in the right place & looks like we expect a navigation item to look. +For example, this is how we could define an extension where its output depends on the configuration: ```tsx -export const HomeNavIcon = createNavItemExtension({ - routeRef: routeRefForTheHomePage, - title: 'Home', - icon: HomeIcon, +const exampleExtension = createExtension({ + // ... + config: { + schema: { + disableIcon: z.boolean().default(false), + }, + }, + output: [coreExtensionData.reactElement, iconDataRef.optional()], + *factory({ config }) { + yield coreExtensionData.reactElement(
Hello World
); + + if (!config.disableIcon) { + yield iconDataRef(); + } + }, }); ``` -### Extension Kind - -With the example `HomeNavIcon` will end up on the extension input `items` of the extensions with the id `app/nav`. It raises the question what the `id` of the `HomeNavIcon` itself is. The extension creator for the navigation item has a defined `kind`, which by convention matches the own name. So in this example `createNavItemExtension` sets the kind to `nav-item`. - -The `id` of the extension is then build out of `namespace`, `name` & `kind` like the following - where `namespace` & `name` are optional properties that can be passed to the extension creator: - -``` -id: kind:namespace/name -``` - -For more information on naming of extension refer to the [naming patterns documentation](./08-naming-patterns.md). - -### Extension Creators in libraries - -Extension creators should be exported from frontend library packages (e.g. `*-react`) rather than plugin packages. - -If an extension is only for in-house tweaks, it's okay to put it in the plugin package. But if you want other open source plugins to use it, or you already have a `-react` package, always put extension creators in the `-react` package. - -## Extension Boundary +## Extension boundary The `ExtensionBoundary` wraps extensions with several React contexts for different purposes ### Suspense -All React elements rendered by extension creators should be wrapped in the extension boundary. With `Suspense` the extension can than load resources asynchronously with having a loading fallback. It also allows to lazy load the whole extension similar to how plugins are currently lazy loaded in Backstage. +Most React elements rendered by extensions should be wrapped in the extension boundary. With `Suspense` the extension can than load resources asynchronously with having a loading fallback. It also allows to lazy load the whole extension similar to how plugins are currently lazy loaded in Backstage. -### Error Boundary +### Error boundary Similar to plugins the `ErrorBoundary` for extension allows to pass in a fallback component in case there is an uncaught error inside of the component. With this the error can be isolated & it would prevent the rest of the plugin to crash. @@ -341,32 +319,21 @@ Similar to plugins the `ErrorBoundary` for extension allows to pass in a fallbac Analytics information are provided through the `AnalyticsContext`, which will give `extensionId` & `pluginId` as context to analytics event fired inside of the extension. Additionally `RouteTracker` will capture an analytics event for routable extension to inform which extension metadata gets associated with a navigation event when the route navigated to is a gathered `mountPoint`. Whether an extension is routable is inferred from its outputs, but you can also explicitly control this behavior by passing the `routable` prop to `ExtensionBoundary`. -The `ExtensionBoundary` can be used like the following in an extension creator: +The `ExtensionBoundary` can be used like the following in an extension: ```tsx -export function createSomeExtension< - TConfig extends {}, - TInputs extends AnyExtensionInputMap, ->(options): ExtensionDefinition { - return createExtension({ - // ... - factory({ config, inputs, node }) { - const ExtensionComponent = lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - return { - path: config.path, - routeRef: options.routeRef, - element: ( - - - - ), - }; - }, - }); -} +const routableExtension = createExtension({ + // ... + factory({ config, inputs, node }) { + return [ + coreExtensionData.path(config.path), + coreExtensionData.routeRef(myRouteRef), + coreExtensionData.reactElement( + + + , + ), + ]; + }, +}); ``` diff --git a/docs/frontend-system/architecture/23-extension-blueprints.md b/docs/frontend-system/architecture/23-extension-blueprints.md new file mode 100644 index 0000000000..ee09e7fec6 --- /dev/null +++ b/docs/frontend-system/architecture/23-extension-blueprints.md @@ -0,0 +1,142 @@ +--- +id: extension-blueprints +title: Frontend Extension Blueprints +sidebar_label: Extensions Blueprints +# prettier-ignore +description: Frontend extensions +--- + +> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** + +The `createExtension` function and related APIs is considered a low-level building and fairly advanced building block, and is not typically what you would use when building plugins and features. Instead, the core APIs and plugins provide extension blueprints that makes it easier to create extensions for specific usages. These blueprints accept a number of parameters that is up to each blueprint to define, and then creates a new extension using the provided parameters. New blueprints are created using the `createExtensionBlueprint` function, and are by convention exported with the symbol `Blueprint`. If you are curious about what blueprints are available from a plugin or package, look for `*Blueprint` exports in the package's API, for plugins these are typically found in the `*-react` package. + +## Creating an extension from a blueprint + +Every extension blueprint provides a `make` method that can be used to create new extensions. It is a simple way to create a new extension where the base blueprint provides all the necessary functionality. All you need to do is to provide the necessary blueprint parameters, but you also have the ability to provide additional options, for example a `name` for the extension. + +The following is a simple example of how one might use the blueprint `make` method to create a new extension: + +```tsx +const myPageExtension = PageBlueprint.make({ + params: { + defaultPath: '/my-page', + loader: () => import('./components/MyPage').them(m => ), + }, +}); +``` + +The returned `myPageExtension` is an extension which is ready to be used in a plugin. It is the same type of object as is returned by the lower level `createExtension` function. + +## Creating an extension from a blueprint with overrides + +Every extension blueprint also provides a `makeWithOverrides` method. It is useful in cases where you want to provide additional integration points for an extension created with a blueprint. You might for example want to define additional inputs or configuration schema, or use the existing configuration to dynamically compute the parameters passed to the blueprint. + +The following is an example of how one might use the blueprint `makeWithOverrides` method to create a new extension: + +```tsx +const myPageExtension = PageBlueprint.makeWithOverrides({ + config: { + schema: { + layout: z => z.enum(['grid', 'rows']).default('grid'), + }, + }, + // The original blueprint factory is provided as the first argument + factory(originalFactory, { config }) { + // Call and forward the result from the original factory, providing + // the blueprint parameters as the first argument. + return originalFactory({ + defaultPath: '/my-page', + loader: () => + import('./components/MyPage').them(m => ( + // We can now access values from the factory context when providing + // the blueprint parameters, such as config values. + + )), + }); + }, +}); +``` + +When using `makeWithOverrides`, we no longer pass the blueprint parameters directly. Instead, we provide a `factory` function that receives the original blueprint factory as the first argument, and the extension factory context as the second. We can then call the original blueprint factory with the blueprint parameters and forward the result as the return value of out factory. Notice that when passing the blueprint parameters using this pattern we have access to a lot more information than when using the `make` method, at the cost of being more complex. + +Apart from the addition of the blueprint parameters of the first argument to the original factory function, the `makeWithOverrides` method works the same way as [extension overrides](./25-extension-overrides.md). All the same options and rules apply, including the ability to define additional inputs, override outputs, and so on. We therefore defer to the [extension overrides](./25-extension-overrides.md) documentation for more information on how to use the `makeWithOverrides` method. + +## Creating an extension blueprint + +To create a new extension blueprint, you use the `createExtensionBlueprint` function. At the surface it is very similar to `createExtension`, but with a few key differences. Firstly you must provide a `kind` option, which will be the kind of all extensions created with the blueprint. See the [naming patterns section](./50-naming-patterns.md) for more information about how to select a good extension kind. Secondly, the `factory` function has a new signature where the first parameter is the blueprint parameters, and the second is the factory context. And finally, rather than returning an extension, `createExtensionBlueprint` returns a blueprint object with the `make` method and friends, which is used as is described above. + +The following is an example of how one might create a new extension blueprint: + +```tsx +export interface MyWidgetBlueprintParams { + defaultTitle: string; + element: JSX.Element; +} + +export const MyWidgetBlueprint = createExtensionBlueprint({ + kind: 'my-widget', + attachTo: { id: 'page:my-plugin', input: 'widgets' }, + config: { + schema: { + title: z.string().optional(), + }, + }, + output: [coreExtensionData.reactElement], + factory(params: MyWidgetBlueprintParams, { config }) { + return [ + // Note that while this is a valid pattern, you might often want to + // return separate pieces of data instead, more on that below. + coreExtensionData.reactElement( + + {params.element} + , + ), + ]; + }, +}); +``` + +This is of course a quite bare-bones example blueprint, but still a very real example. Blueprints can be very simple, there's already a lot of value in encapsulating the extension kind, attachment point, and output in a blueprint. + +Most of the options provided to `createExtensionBlueprint` can be overridden when using `makeWithOverrides` to create an extension from the blueprint. These overrides work the same way as [extension overrides](./25-extension-overrides.md), and we defer to that documentation for more information on how overrides work. + +### Blueprint-specific extension data references + +In some cases you may want to define and provide [extension data reference](./20-extensions.md#extension-data-references) that are specific to your blueprint. In the above example we might want to forward the `title` as data for example, rather than encapsulating it into the `MyWidgetContainer` component. This gives the parent extension more flexibility in the rendering for our example widget extensions. + +To do that, we create a new extension data reference for our widget title. This references is provided via the `dataRefs` options when we create the blueprint, which makes it available for use via `MyWidgetBlueprint.dataRefs.widgetTitle`. + +```tsx +export interface MyWidgetBlueprintParams { + defaultTitle: string; + element: JSX.Element; +} + +const widgetTitleRef = createExtensionDataRef().with({ + id: 'my-plugin.widget.title', +}); + +export const MyWidgetBlueprint = createExtensionBlueprint({ + kind: 'my-widget', + attachTo: { id: 'page:my-plugin', input: 'widgets' }, + config: { + schema: { + title: z.string().optional(), + }, + }, + output: [widgetTitleRef, coreExtensionData.reactElement], + factory(params: MyWidgetBlueprintParams, { config }) { + return [ + widgetTitleRef(config.title ?? params.defaultTitle), + coreExtensionData.reactElement(params.element), + ]; + }, + dataRefs: { + widgetTitle: widgetTitleRef, + }, +}); +``` + +### Extension Blueprints in libraries + +If you are publishing a plugin, the extension creators should always be exported from frontend library packages (e.g. `*-react`) rather than plugin packages. diff --git a/docs/frontend-system/architecture/05-extension-overrides.md b/docs/frontend-system/architecture/25-extension-overrides.md similarity index 99% rename from docs/frontend-system/architecture/05-extension-overrides.md rename to docs/frontend-system/architecture/25-extension-overrides.md index 41702e6823..42a7f365a1 100644 --- a/docs/frontend-system/architecture/05-extension-overrides.md +++ b/docs/frontend-system/architecture/25-extension-overrides.md @@ -89,7 +89,7 @@ Note that it can still be a good idea to split your overrides out into separate To override an extension that is provided by a plugin, you need to provide a new extension that has the same ID as the existing extension. That is, all kind, namespace, and name options must match the extension you want to replace. This means that you typically need to provide an explicit `namespace` when overriding extensions from a plugin. :::info -We recommend that plugin developers share the extension IDs in their plugin documentation, but usually you can infer the ID by following the [naming patterns](./08-naming-patterns.md) documentation. +We recommend that plugin developers share the extension IDs in their plugin documentation, but usually you can infer the ID by following the [naming patterns](./50-naming-patterns.md) documentation. ::: ### Example diff --git a/docs/frontend-system/architecture/08-references.md b/docs/frontend-system/architecture/30-references.md similarity index 100% rename from docs/frontend-system/architecture/08-references.md rename to docs/frontend-system/architecture/30-references.md diff --git a/docs/frontend-system/architecture/06-utility-apis.md b/docs/frontend-system/architecture/33-utility-apis.md similarity index 97% rename from docs/frontend-system/architecture/06-utility-apis.md rename to docs/frontend-system/architecture/33-utility-apis.md index 49e5854b65..c20936f819 100644 --- a/docs/frontend-system/architecture/06-utility-apis.md +++ b/docs/frontend-system/architecture/33-utility-apis.md @@ -10,7 +10,7 @@ description: Utility APIs ## Overview -Utility APIs are pieces of standalone functionality, interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/03-extensions.md) that can have inputs, be replaced, and be declaratively configured in your app-config. +Utility APIs are pieces of standalone functionality, interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/20-extensions.md) that can have inputs, be replaced, and be declaratively configured in your app-config. A common example of a utility API is a client interface to interact with the backend part of a plugin, such as the catalog client. Any frontend plugin can then request an implementation of that interface to make requests through. diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/36-routes.md similarity index 90% rename from docs/frontend-system/architecture/07-routes.md rename to docs/frontend-system/architecture/36-routes.md index 9c6617a06f..e684932414 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/36-routes.md @@ -42,7 +42,7 @@ The code snippet in the previous section does not indicate which plugin the rout ```tsx title="plugins/catalog/src/plugin.tsx" import React from 'react'; import { - createPlugin, + createFrontendPlugin, createPageExtension, } from '@backstage/frontend-plugin-api'; import { indexRouteRef } from './routes'; @@ -55,7 +55,7 @@ const catalogIndexPage = createPageExtension({ loader: () => import('./components').then(m => ), }); -export default createPlugin({ +export default createFrontendPlugin({ id: 'catalog', // highlight-start routes: { @@ -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 */}
); }; @@ -190,7 +196,7 @@ Now the only thing left is to provide the page and external route via a plugin: ```tsx title="plugins/catalog/src/plugin.tsx" import React from 'react'; import { - createPlugin, + createFrontendPlugin, createPageExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; @@ -202,7 +208,7 @@ const catalogIndexPage = createPageExtension({ loader: () => import('./components').then(m => ), }); -export default createPlugin({ +export default createFrontendPlugin({ id: 'catalog', routes: { index: indexRouteRef, @@ -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 */} } />
@@ -429,7 +404,7 @@ Finally, see how a plugin can provide subroutes: ```tsx title="plugins/catalog/src/plugin.tsx" import React from 'react'; import { - createPlugin, + createFrontendPlugin, createPageExtension, } from '@backstage/frontend-plugin-api'; import { indexRouteRef, detailsSubRouteRef } from './routes'; @@ -440,7 +415,7 @@ const catalogIndexPage = createPageExtension({ loader: () => import('./components').then(m => ), }); -export default createPlugin({ +export default createFrontendPlugin({ id: 'catalog', routes: { index: indexRouteRef, diff --git a/docs/frontend-system/architecture/08-naming-patterns.md b/docs/frontend-system/architecture/50-naming-patterns.md similarity index 91% rename from docs/frontend-system/architecture/08-naming-patterns.md rename to docs/frontend-system/architecture/50-naming-patterns.md index f031b80d84..11e0b8e733 100644 --- a/docs/frontend-system/architecture/08-naming-patterns.md +++ b/docs/frontend-system/architecture/50-naming-patterns.md @@ -23,7 +23,7 @@ Example: ```ts // This declaration is only for internal usage in tests. This could also be a direct default export. -export const userSettingsPlugin = createPlugin({ +export const userSettingsPlugin = createFrontendPlugin({ id: 'user-settings', ... }) @@ -42,7 +42,7 @@ Note that while we use this naming pattern for the plugin instance this is only | ID | `[:][/]` | `'core.nav'`, `'page:user-settings'`, `'entity-card:catalog/about'` | | Symbol | `[][]` | `coreNav`, `userSettingsPage`, `catalogAboutEntityCard` | -When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the blueprint creator, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. +When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the extension blueprint, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. Example: @@ -67,7 +67,7 @@ const catalogSearchResultListItem = SearchResultListItemBlueprint.make({ }); // Note that the extensions themselves are not exported, only the plugin instance -export const catalogPlugin = createPlugin({ +export const catalogPlugin = createFrontendPlugin({ id: 'catalog', extensions: [catalogEntityPage, catalogSearchResultListItem /* ... */], }); diff --git a/docs/frontend-system/architecture/60-migrations.md b/docs/frontend-system/architecture/60-migrations.md new file mode 100644 index 0000000000..a1ab0197f7 --- /dev/null +++ b/docs/frontend-system/architecture/60-migrations.md @@ -0,0 +1,104 @@ +--- +id: migrations +title: Frontend System Migrations +sidebar_label: Migrations +# prettier-ignore +description: Migration documentation for different versions of the frontend system core APIs. +--- + +> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** + +## Introduction + +This section provides migration guides for different versions of the frontend system core APIs. Each guide will provide a summary of the changes that have been made for a particular Backstage release, and how to update your usage of the core APIs. + +This guide is intended for app and plugin authors who have already migrated their code to the new frontend system, and are looking to keep it up to date with the latest changes. These guides do not cover trivial migrations that can be explained in a deprecation message, such as a renamed export. + +## 1.30 + +### Reworked extension inputs and outputs + +In previous versions of the frontend system you would define extension inputs and outputs as an "data map" where each named property corresponded to a piece of data that could be accessed via the same name in the extension factory. In order to better support extension overrides and blueprints, as well as reduce the risk of confusion, these data maps have been replaced by an array of data references. + +For example, an extension previously declared like this: + +```tsx +createExtension({ + name: 'example', + attachTo: { id: 'some-extension', input: 'content' }, + inputs: { + header: createExtensionInput( + { element: coreExtensionData.reactElement }, + { + optional: true, + singleton: true, + }, + ), + }, + output: { element: coreExtensionData.reactElement }, + factory({ inputs }) { + return { + element: , + }; + }, +}); +``` + +Would now look like this: + +```tsx +createExtension({ + name: 'example', + attachTo: { id: 'some-extension', input: 'content' }, + inputs: { + header: createExtensionInput([coreExtensionData.reactElement], { + optional: true, + singleton: true, + }), + }, + output: [coreExtensionData.reactElement], + factory({ inputs }) { + return [ + coreExtensionData.reactElement( + , + ), + ]; + }, +}); +``` + +Note the changes to the `inputs` and `output` declarations, as well as how these are used in the factory. Both the `inputs` and `output` now declare their expected data using an array, without names tied to each piece of data. The `get` method on the input object is used to access the input data, and accept a data reference matching the data that was declared for that input. + +The biggest change in practice is how the extension factories output their data. Instead of returning an object with the data values, you instead use each extension data reference to encapsulate a value and return a collection of these encapsulated values from the factory. + +### Blueprints instead of extension creators + +The "extension creator" pattern where `createExtension` was wrapped up in a function for creating specific kind of extensions has been replaced by [extension blueprints](./23-extension-blueprints.md). Extension creators were hard to implement and to maintain, with blueprints providing a much more consistent and powerful API surface for both plugin builders and integrators. For example, `createPageExtension` was the extension creator equivalent of `PageBlueprint`. + +Replace all existing usages of extension creators in your plugin or app with the corresponding blueprint. For a given extension creator `createExtension`, the blueprint equivalent is `Blueprint`. There might be slight differences in the API between the two, but most often you just need to move the kind-specific options of the extension creator to be passed as parameters to the blueprint. Note that if your extension declared any additional inputs or config you'll need to use the [`.makeWithOverrides`](./23-extension-blueprints.md#creating-an-extension-from-a-blueprint-with-overrides) method of the blueprint. + +If your plugin exports and extension creators, these should be migrated to blueprints instead. + +### Extension tester rework + +The `createExtensionTester` from the `@backstage/frontend-test-utils` package has been reworked to better support testing of extensions. The new API allows access to extension output directly using the new `.get(ref)` method, and also provides access to all tested extensions through the new `.query(id/extension)` method. + +The `.render()` method that used to render the test subject in a test app has been deprecated. The extension tester no longer constructs a full app tree, but instead only instantiates the tree for the extensions under test. If you want to test the rendering of an extension that outputs in an app, you can instead use the `renderInTestApp` utility in combination with the new `.reactElement()` method of the extension tester: `renderInTestApp(tester.reactElement())`. + +### Extension data references update + +The way that extension data references are declared has been changed to allow for type inference of the ID. This requires the declaration to be split into two separate function calls, one to supply the type and the other to infer any options. For example, a reference that was previously declared like this: + +```ts +export const myExtension = createExtensionDataRef('my-plugin.my-data'); +``` + +Should be updated to the following: + +```ts +export const myExtension = createExtensionDataRef().with({ + id: 'my-plugin.my-data', +}); +``` diff --git a/docs/frontend-system/building-apps/01-index.md b/docs/frontend-system/building-apps/01-index.md index ea7fe20948..bb09fb6907 100644 --- a/docs/frontend-system/building-apps/01-index.md +++ b/docs/frontend-system/building-apps/01-index.md @@ -66,7 +66,7 @@ Linking routes from different plugins requires this configuration. You can do th ### Enable feature discovery -Use this setting to enable experimental feature discovery when building your app with `@backstage/cli`. With this configuration your application tries to discover and install package extensions automatically, check [here](../architecture/02-app.md#feature-discovery) for more details. +Use this setting to enable experimental feature discovery when building your app with `@backstage/cli`. With this configuration your application tries to discover and install package extensions automatically, check [here](../architecture/10-app.md#feature-discovery) for more details. :::warning Remember that package extensions that are not auto-discovered must be manually added to the application when creating an app. See [features](#install-features-manually) for more details. @@ -99,7 +99,7 @@ export default app.createRoot(); ``` :::info -You can also pass overrides to the features array, for more details, please read the [extension overrides](../architecture/05-extension-overrides.md) documentation. +You can also pass overrides to the features array, for more details, please read the [extension overrides](../architecture/25-extension-overrides.md) documentation. ::: ### Using an async features loader diff --git a/docs/frontend-system/building-apps/03-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md index a5e6476eb0..5f74100df0 100644 --- a/docs/frontend-system/building-apps/03-built-in-extensions.md +++ b/docs/frontend-system/building-apps/03-built-in-extensions.md @@ -24,7 +24,7 @@ Be careful when disabling built-in extensions, as there may be other extensions ## Override built-in extensions -You can override any built-in extension whenever their customizations, whether through configuration or input, do not meet a use case for your Backstage instance. Check out [this](../architecture/05-extension-overrides.md) documentation on how to override application extensions. +You can override any built-in extension whenever their customizations, whether through configuration or input, do not meet a use case for your Backstage instance. Check out [this](../architecture/25-extension-overrides.md) documentation on how to override application extensions. :::warning Be aware there could be some implementation requirements to properly override an built-in extension, such as using same apis and do not remove inputs or configurations otherwise you can cause a side effect in other parts of the system that expects same minimal behavior. diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 716e0da772..b8fd49c269 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -23,7 +23,7 @@ import { createApp } from '@backstage/frontend-app-api'; This immediate switch will lead to a lot of breakages that we need to fix. -Let's start by addressing the change to `app.createRoot(...)`, which no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/01-index.md) section. +Let's start by addressing the change to `app.createRoot(...)`, which no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/00-index.md) section. Given that the app element tree is most of what builds up the app, it's likely also going to be the majority of the migration effort. In order to make the migration as smooth as possible we have provided a helper that lets you convert an existing app element tree into plugins that you can install in a new app. This in turn allows for a gradual migration of individual plugins, rather than needing to migrate the entire app structure at once. @@ -95,7 +95,7 @@ At this point the contents of your app should be past the initial migration stag ## Migrating `createApp` Options -Many of the `createApp` options have been migrated to use extensions instead. Each will have their own [extension creator](../architecture/03-extensions.md#extension-creators) that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to `createExtensionOverrides`, which bundles them into a _feature_ that you can install in the app. See the [standalone extensions](../architecture/05-extension-overrides.md#create-standalone-extensions) section for more information. +Many of the `createApp` options have been migrated to use extensions instead. Each will have their own [extension creator](../architecture/20-extensions.md#extension-creators) that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to `createExtensionOverrides`, which bundles them into a _feature_ that you can install in the app. See the [standalone extensions](../architecture/25-extension-overrides.md#create-standalone-extensions) section for more information. For example, assuming you have a `lightTheme` extension that you want to add to your app, you can use the following: @@ -176,7 +176,7 @@ createApp({ }); ``` -Plugins don't even have to be imported manually after installing their package if [features discovery](../architecture/02-app.md#feature-discovery) is enabled. +Plugins don't even have to be imported manually after installing their package if [features discovery](../architecture/10-app.md#feature-discovery) is enabled. ```yaml title="in app-config.yaml" app: @@ -207,7 +207,7 @@ createApp({ Can be converted to the following plugin configuration: ```tsx -createPlugin({ +createFrontendPlugin({ id: 'tech-radar', // ... featureFlags: [{ name: 'tech-radar' }], @@ -219,7 +219,7 @@ createPlugin({ Many app components are now installed as extensions instead using `createComponentExtension`. See the section on [configuring app components](./01-index.md#configure-your-app) for more information. -The `Router` component is now a built-in extension that you can [override](../architecture/05-extension-overrides.md) using `createRouterExtension`. +The `Router` component is now a built-in extension that you can [override](../architecture/25-extension-overrides.md) using `createRouterExtension`. The Sign-in page is now installed as an extension using the `createSignInPageExtension` instead. @@ -341,7 +341,7 @@ const app = createApp({ ### `bindRoutes` -Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/07-routes.md#binding-external-route-references) for more information. +Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/36-routes.md#binding-external-route-references) for more information. Note that if you are binding routes from a legacy plugin that was converted using `convertLegacyApp`, you will need to use the `convertLegacyRouteRefs` and/or `convertLegacyRouteRef` to convert the routes to be compatible with the new system. @@ -470,7 +470,7 @@ const routes = ( ); ``` -If you are using [app feature discovery](../architecture/02-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually install the new scaffolder plugin in your app through the `features` option of `createApp`. +If you are using [app feature discovery](../architecture/10-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually install the new scaffolder plugin in your app through the `features` option of `createApp`. Continue this process for each of your legacy routes until you have migrated all of them. For any plugin with additional extensions installed as children of the `Route`, refer to the plugin READMEs for more detailed instructions. For the entity pages, refer to the [separate section](#entity-pages). @@ -482,7 +482,7 @@ The entity pages are typically defined in `packages/app/src/components/catalog` New apps feature a built-in sidebar extension (`app/nav`) that will render all nav item extensions provided by plugins. This is a placeholder implementation and not intended as a long-term solution. In the future we will aim to provide a more flexible sidebar extension that allows for more customization out of the box. -Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an [extension override](../architecture/05-extension-overrides.md): +Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an [extension override](../architecture/25-extension-overrides.md): ```tsx const nav = createExtension({ diff --git a/docs/frontend-system/building-plugins/01-index.md b/docs/frontend-system/building-plugins/01-index.md index f8d80f9382..355de26616 100644 --- a/docs/frontend-system/building-plugins/01-index.md +++ b/docs/frontend-system/building-plugins/01-index.md @@ -8,9 +8,9 @@ description: Building frontend plugins using the new frontend system > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -This section covers how to build your own frontend [plugins](../architecture/04-plugins.md) and -[overrides](../architecture/05-extension-overrides.md). They are sometimes collectively referred to as -frontend _features_, and what you install to build up a Backstage frontend [app](../architecture/02-app.md). +This section covers how to build your own frontend [plugins](../architecture/15-plugins.md) and +[overrides](../architecture/25-extension-overrides.md). They are sometimes collectively referred to as +frontend _features_, and what you install to build up a Backstage frontend [app](../architecture/10-app.md). ## Creating a new plugin @@ -24,14 +24,14 @@ The created plugin will currently be templated for use in the legacy frontend sy ## The plugin instance -The starting point of a frontend plugin is the `createPlugin` function, which accepts a single options object as its only parameter. It is imported from `@backstage/frontend-plugin-api`, which is where you will find most of the common APIs for building plugins. +The starting point of a frontend plugin is the `createFrontendPlugin` function, which accepts a single options object as its only parameter. It is imported from `@backstage/frontend-plugin-api`, which is where you will find most of the common APIs for building plugins. This is how to create a minimal plugin: ```tsx title="in src/plugin.ts" -import { createPlugin } from '@backstage/frontend-plugin-api'; +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; -export const examplePlugin = createPlugin({ +export const examplePlugin = createFrontendPlugin({ id: 'example', extensions: [], }); @@ -43,13 +43,13 @@ export { examplePlugin as default } from './plugin'; Note that we export the plugin as the default export of our package from `src/index.ts`. This is important, as it is how users of our plugin are able to seamlessly install the plugin package in a Backstage app without having to reference the plugin instance through code. -The plugin ID should be a lowercase dash-separated string, while the plugin instance variable should be the camel case version of the ID with a `Plugin` suffix. For more details on naming patterns within the frontend system, see [the article on naming patterns](../architecture/08-naming-patterns.md). By sticking to these naming patterns you ensure that users of your plugin more easily recognize the exports and features provided by your plugin. +The plugin ID should be a lowercase dash-separated string, while the plugin instance variable should be the camel case version of the ID with a `Plugin` suffix. For more details on naming patterns within the frontend system, see [the article on naming patterns](../architecture/50-naming-patterns.md). By sticking to these naming patterns you ensure that users of your plugin more easily recognize the exports and features provided by your plugin. ## Adding extensions -The plugin that we created above is empty, and doesn't provide any actual functionality. To add functionality to a plugin you need to create and provide it with one or more [extensions](../architecture/03-extensions.md). Let's continue by adding a standalone page to our plugin, as well as a navigation item that allows users to navigate to the page. +The plugin that we created above is empty, and doesn't provide any actual functionality. To add functionality to a plugin you need to create and provide it with one or more [extensions](../architecture/20-extensions.md). Let's continue by adding a standalone page to our plugin, as well as a navigation item that allows users to navigate to the page. -To create a new extension you typically use pre-defined [extension creators](../architecture/03-extensions.md#extension-creators), provided either by the framework itself or by other plugins. In this case we'll need to use `createPageExtension` and `createNavItemExtension`, both from `@backstage/frontend-plugin-api`. We will also need to [create a route reference](../architecture/07-routes.md#creating-a-route-reference) to use as a reference for out page, allowing us to dynamically create URLs that link to our page. +To create a new extension you typically use pre-defined [extension creators](../architecture/20-extensions.md#extension-creators), provided either by the framework itself or by other plugins. In this case we'll need to use `createPageExtension` and `createNavItemExtension`, both from `@backstage/frontend-plugin-api`. We will also need to [create a route reference](../architecture/36-routes.md#creating-a-route-reference) to use as a reference for out page, allowing us to dynamically create URLs that link to our page. ```tsx title="in src/routes.ts" import { createRouteRef } from '@backstage/frontend-plugin-api'; @@ -63,7 +63,7 @@ export const rootRouteRef = createRouteRef(); ```tsx title="in src/plugin.ts" import { - createPlugin, + createFrontendPlugin, createPageExtension, createNavItemExtension, } from '@backstage/frontend-plugin-api'; @@ -92,7 +92,7 @@ const exampleNavItem = createNavItemExtension({ }); // The same plugin as above, now with the extensions added -export const examplePlugin = createPlugin({ +export const examplePlugin = createFrontendPlugin({ id: 'example', extensions: [examplePage, exampleNavItem], // We can also make routes available to other plugins. @@ -106,7 +106,7 @@ export const examplePlugin = createPlugin({ What we've built here is a very common type of plugin. It's a top-level tool that provides a single page, along with a method for navigating to that page. The implementation of the page component, in this case the highlighted `ExamplePage`, can be arbitrarily complex. It can be anything from a single simple information page, to a full-blown application with multiple sub-pages. -We have also provided external access to our route reference by passing it to the plugin `routes` option. This makes it possible for app integrators to bind an external link from a different plugin to our plugin page. You can read more about how this works in the [External Route References](../architecture/07-routes.md#external-route-references) section. +We have also provided external access to our route reference by passing it to the plugin `routes` option. This makes it possible for app integrators to bind an external link from a different plugin to our plugin page. You can read more about how this works in the [External Route References](../architecture/36-routes.md#external-route-references) section. ## Utility APIs @@ -168,7 +168,7 @@ const exampleApi = createApiExtension({ /* Omitted definitions for examplePage, exampleNavItem, and rootRouteRef. */ -export const examplePlugin = createPlugin({ +export const examplePlugin = createFrontendPlugin({ id: 'example', extensions: [ // highlight-add-next-line @@ -202,7 +202,7 @@ const exampleEntityContent = createEntityContentExtension({ )), }); -export const examplePlugin = createPlugin({ +export const examplePlugin = createFrontendPlugin({ id: 'example', extensions: [ // highlight-add-next-line diff --git a/docs/frontend-system/building-plugins/02-testing.md b/docs/frontend-system/building-plugins/02-testing.md index 26c9503c6d..f665ed0f0c 100644 --- a/docs/frontend-system/building-plugins/02-testing.md +++ b/docs/frontend-system/building-plugins/02-testing.md @@ -41,7 +41,7 @@ describe('Entity details component', () => { }); ``` -To mock [Utility APIs](../architecture/06-utility-apis.md) that are used by your component you can use the `TestApiProvider` to override individual API implementations. In the snippet below, we wrap the component within a `TestApiProvider` in order to mock the catalog client API: +To mock [Utility APIs](../architecture/33-utility-apis.md) that are used by your component you can use the `TestApiProvider` to override individual API implementations. In the snippet below, we wrap the component within a `TestApiProvider` in order to mock the catalog client API: ```tsx import React from 'react'; diff --git a/docs/frontend-system/building-plugins/03-extension-types.md b/docs/frontend-system/building-plugins/03-extension-types.md index 1f3baaa710..c7c2e2ab1f 100644 --- a/docs/frontend-system/building-plugins/03-extension-types.md +++ b/docs/frontend-system/building-plugins/03-extension-types.md @@ -8,7 +8,7 @@ description: Extension types provided by the frontend system and core features > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -This section covers many of the [extension types](../architecture/03-extensions.md#extension-creators) available at your disposal when building Backstage frontend plugins. +This section covers many of the [extension types](../architecture/20-extensions.md#extension-creators) available at your disposal when building Backstage frontend plugins. ## Built-in extension types diff --git a/docs/frontend-system/building-plugins/04-built-in-data-refs.md b/docs/frontend-system/building-plugins/04-built-in-data-refs.md index dacbbf30f1..d13e734279 100644 --- a/docs/frontend-system/building-plugins/04-built-in-data-refs.md +++ b/docs/frontend-system/building-plugins/04-built-in-data-refs.md @@ -8,7 +8,7 @@ description: Configuring or overriding built-in extension data references > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -To have a better understanding of extension data references please read [the corresponding architecture section](../architecture/03-extensions.md#extension-data) first. +To have a better understanding of extension data references please read [the corresponding architecture section](../architecture/20-extensions.md#extension-data) first. ## Built-in extension data references diff --git a/docs/frontend-system/building-plugins/05-migrating.md b/docs/frontend-system/building-plugins/05-migrating.md index 16f465ece7..79b259a2e5 100644 --- a/docs/frontend-system/building-plugins/05-migrating.md +++ b/docs/frontend-system/building-plugins/05-migrating.md @@ -15,9 +15,9 @@ The main concept is that routes, components, apis are now extensions. You can us In the legacy frontend system a plugin was defined in its own `plugin.ts` file as following: ```ts title="my-plugin/src/plugin.ts" - import { createPlugin } from '@backstage/core-plugin-api'; + import { createFrontendPlugin } from '@backstage/core-plugin-api'; - export const myPlugin = createPlugin({ + export const myPlugin = createFrontendPlugin({ id: 'my-plugin', apis: [], routes: { @@ -29,13 +29,13 @@ In the legacy frontend system a plugin was defined in its own `plugin.ts` file a }); ``` -In order to migrate the actual definition of the plugin you need to recreate the plugin using the new `createPlugin` utility exported by `@backstage/frontend-plugin-api`. -The new `createPlugin` function doesn't accept apis anymore as apis are now extensions. +In order to migrate the actual definition of the plugin you need to recreate the plugin using the new `createFrontendPlugin` utility exported by `@backstage/frontend-plugin-api`. +The new `createFrontendPlugin` function doesn't accept apis anymore as apis are now extensions. ```ts title="my-plugin/src/alpha.ts" - import { createPlugin } from '@backstage/frontend-plugin-api'; + import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; - export default createPlugin({ + export default createFrontendPlugin({ id: 'my-plugin', // bind all the extensions to the plugin /* highlight-next-line */ @@ -118,9 +118,9 @@ const fooPage = createPageExtension({ then add the `fooPage` extension to the plugin: ```ts title="my-plugin/src/alpha.ts" - import { createPlugin } from '@backstage/frontend-plugin-api'; + import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; - export default createPlugin({ + export default createFrontendPlugin({ id: 'my-plugin', // bind all the extensions to the plugin /* highlight-remove-next-line */ @@ -207,9 +207,9 @@ const exampleWorkApi = createApiExtension({ Finally, let's add the `exampleWorkApi` extension to the plugin: ```ts title="my-plugin/src/alpha.ts" - import { createPlugin } from '@backstage/frontend-plugin-api'; + import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; - export default createPlugin({ + export default createFrontendPlugin({ id: 'my-plugin', // bind all the extensions to the plugin /* highlight-remove-next-line */ diff --git a/docs/frontend-system/utility-apis/01-index.md b/docs/frontend-system/utility-apis/01-index.md index 37120a5152..07f7dcc2a8 100644 --- a/docs/frontend-system/utility-apis/01-index.md +++ b/docs/frontend-system/utility-apis/01-index.md @@ -8,7 +8,7 @@ description: Working with Utility APIs in the New Frontend System > **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.** -As described [in the architecture section](../architecture/06-utility-apis.md), utility APIs are pieces of shared functionality - interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/03-extensions.md) that can accept inputs, be declaratively configured in your app-config, or transparently be replaced entirely with custom implementations that fulfill the same contract. +As described [in the architecture section](../architecture/33-utility-apis.md), utility APIs are pieces of shared functionality - interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/20-extensions.md) that can accept inputs, be declaratively configured in your app-config, or transparently be replaced entirely with custom implementations that fulfill the same contract. ## Creating utility APIs diff --git a/docs/frontend-system/utility-apis/02-creating.md b/docs/frontend-system/utility-apis/02-creating.md index fcb7ff1a65..bd0b71120d 100644 --- a/docs/frontend-system/utility-apis/02-creating.md +++ b/docs/frontend-system/utility-apis/02-creating.md @@ -47,7 +47,7 @@ The plugin itself now wants to provide this API and its default implementation, import { createApiExtension, createApiFactory, - createPlugin, + createFrontendPlugin, storageApiRef, StorageApi, } from '@backstage/frontend-plugin-api'; @@ -76,7 +76,7 @@ const exampleWorkApi = createApiExtension({ * The Example plugin. * @public */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'example', extensions: [exampleWorkApi], }); @@ -86,7 +86,7 @@ For illustration we make a skeleton implementation class and the API extension a The code also illustrates how the API factory declares a dependency on another utility API - the core storage API in this case. An instance of that utility API is then provided to the factory function. -The resulting extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, in this case ending up as `api:plugin.example.work`. Check out [the naming patterns doc](../architecture/08-naming-patterns.md) for more information on how this works. You can now use this ID to refer to the API in app-config and elsewhere. +The resulting extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, in this case ending up as `api:plugin.example.work`. Check out [the naming patterns doc](../architecture/50-naming-patterns.md) for more information on how this works. You can now use this ID to refer to the API in app-config and elsewhere. ## Adding configurability diff --git a/docs/frontend-system/utility-apis/04-configuring.md b/docs/frontend-system/utility-apis/04-configuring.md index 7acf23decf..d22e10011b 100644 --- a/docs/frontend-system/utility-apis/04-configuring.md +++ b/docs/frontend-system/utility-apis/04-configuring.md @@ -12,7 +12,7 @@ Utility APIs are extensions and can therefore optionally be amended with configu ## Configuring -To configure your Utility API extension, first you'll need to know its ID. That ID is formed from the API ref ID; check [the naming patterns docs](../architecture/08-naming-patterns.md) for details. +To configure your Utility API extension, first you'll need to know its ID. That ID is formed from the API ref ID; check [the naming patterns docs](../architecture/50-naming-patterns.md) for details. Our example work API from [the creating section](./02-creating.md) would have the ID `api:plugin.example.work`. You configure it and all other extensions under the `app.extensions` section of your app-config. @@ -37,7 +37,7 @@ Well written input-enabled extension often have extension creator functions that ## Replacing a Utility API implementation -Like with other extension types, you replace Utility APIs with your own custom implementation using [extension overrides](../architecture/05-extension-overrides.md). +Like with other extension types, you replace Utility APIs with your own custom implementation using [extension overrides](../architecture/25-extension-overrides.md). ```tsx title="in your app" /* highlight-add-start */ 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/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index 0f2d4fe091..0762f6b0c6 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -141,4 +141,8 @@ class TestPermissionPolicy implements PermissionPolicy { In this example, we use [`isResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.isresourcepermission) to match all permissions with a resource type of `catalog-entity`. Just like `isPermission`, this helper will "narrow" the type of `request.permission` and enable the use of `createCatalogConditionalDecision`. In addition to the behavior you observed before, you should also see that catalog entities are no longer visible unless you are the owner - success! -_Note:_ Some catalog permissions do not have the `'catalog-entity'` resource type, such as [`catalogEntityCreatePermission`](https://github.com/backstage/backstage/blob/1e5e9fb9de9856a49e60fc70c38a4e4e94c69570/plugins/catalog-common/src/permissions.ts#L49). In those cases, a definitive decision is required because conditions can't be applied to an entity that does not exist yet. +:::note Note + +Some catalog permissions do not have the `'catalog-entity'` resource type, such as [`catalogEntityCreatePermission`](https://github.com/backstage/backstage/blob/1e5e9fb9de9856a49e60fc70c38a4e4e94c69570/plugins/catalog-common/src/permissions.ts#L49). In those cases, a definitive decision is required because conditions can't be applied to an entity that does not exist yet. + +::: diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 1a9c0babf9..a0e0e2d5e6 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -329,7 +329,7 @@ You can also use static configuration to bind routes, removing the need to make changes to the app code. It does however mean that you won't get type safety when binding routes and compile-time validation of the bindings. Static configuration of route bindings is done under the `app.routes.bindings` key in -`app-config.yaml`. It works the same way as [route bindings in the new frontend system](../frontend-system/architecture/07-routes.md#binding-external-route-references), +`app-config.yaml`. It works the same way as [route bindings in the new frontend system](../frontend-system/architecture/36-routes.md#binding-external-route-references), for example: ```yaml @@ -342,7 +342,7 @@ app: ### Default Targets for External Route References Following the `1.28` release of Backstage you can now define default targets for -external route references. They work the same way as [default targets in the new frontend system](../frontend-system/architecture/07-routes.md#default-targets-for-external-route-references), +external route references. They work the same way as [default targets in the new frontend system](../frontend-system/architecture/36-routes.md#default-targets-for-external-route-references), for example: ```ts 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.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/docs/releases/v1.30.0-next.4-changelog.md b/docs/releases/v1.30.0-next.4-changelog.md new file mode 100644 index 0000000000..433b5f894c --- /dev/null +++ b/docs/releases/v1.30.0-next.4-changelog.md @@ -0,0 +1,2113 @@ +# Release v1.30.0-next.4 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.4](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.4) + +## @backstage/catalog-model@1.6.0-next.0 + +### Minor Changes + +- 34fa803: Introduce an optional spec.type attribute on the Domain and System entity kinds + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.2.0-next.3 + +### Minor Changes + +- 75d026a: Support for Cloudflare Custom Headers and Custom Cookie Auth Name + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-notifications@0.3.0-next.1 + +### Minor Changes + +- 0410fc9: By default, set notification as read when opening snackbar or web notification link + +### Patch Changes + +- 80b84f7: Fixed issue with notification reloading on page change +- Updated dependencies + - @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-notifications-common@0.0.5 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-scaffolder@1.24.0-next.3 + +### Minor Changes + +- 1552c33: Changed the way to display entities in `MyGroupsPicker` to use `entityPresentationApi` and make it consistent across scaffolder pickers + +### Patch Changes + +- 47ed51b: Add an extra bit of height to the EntityPicker dropdown to make it clear there are more options to select from, and to remove the scroll bar when there is less than 10 options +- d18f4eb: Fix undefined in the title of Scaffolder Runs on the page load +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + - @backstage/plugin-scaffolder-react@1.11.0-next.3 + +## @backstage/backend-app-api@0.8.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/backend-common@0.23.4-next.3 + +### Patch Changes + +- ddde5fe: Internal type refactor. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/backend-defaults@0.4.2-next.3 + +### Patch Changes + +- 81f930a: use formatted query to prevent chance of SQL-injection +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-app-api@0.8.1-next.3 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/backend-dynamic-feature-service@0.2.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-app-api@0.8.1-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-events-backend@0.3.10-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/backend-openapi-utils@0.1.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/errors@1.2.4 + +## @backstage/backend-plugin-api@0.8.0-next.3 + +### Patch Changes + +- ddde5fe: Fixed a type issue where plugin and modules depending on multiton services would not receive the correct type. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/backend-tasks@0.5.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/backend-app-api@0.8.1-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/catalog-client@1.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + +## @backstage/cli@0.27.0-next.4 + +### Patch Changes + +- 6d898d8: Switched the `process` polyfill to use `require.resolve` for greater compatability. +- 2ced236: Updated dependency `@module-federation/enhanced` to `0.3.1` +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.14.0-next.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.18-next.4 + +### Patch Changes + +- bfeba46: Included permission config and enabled it out of the box +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.37-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @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-plugin-api@0.7.0-next.3 + +### Patch Changes + +- 6f72c2b: Fixing issue with extension blueprints `inputs` merging. + +- 99abb6b: Support overriding of plugin extensions using the new `plugin.withOverrides` method. + + ```tsx + import homePlugin from '@backstage/plugin-home'; + + export default homePlugin.withOverrides({ + extensions: [ + homePage.getExtension('page:home').override({ + *factory(originalFactory) { + yield* originalFactory(); + yield coreExtensionData.reactElement(

My custom home page

); + }, + }), + ], + }); + ``` + +- a65cfc8: 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. + +- 34f1b2a: 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. + +- 2d21599: 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)} + + ); + } + }); + + ``` + +- 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/frontend-test-utils@0.1.12-next.3 + +### Patch Changes + +- 2d21599: 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)} + + ); + } + }); + + ``` + +- 264e10f: Deprecate existing `ExtensionCreators` in favour of their new Blueprint counterparts. + +- 264e10f: 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. + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.3 + - @backstage/config@1.2.0 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/plugin-api-docs@0.11.8-next.3 + +### Patch Changes + +- 6582799: Add `tableOptions` to all tables and additionally `title` to API tables. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + +## @backstage/plugin-app-backend@0.3.72-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-app-node@0.1.23-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config-loader@1.9.0-next.2 + +## @backstage/plugin-app-visualizer@0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.2.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.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.3 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.3 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.3 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.3 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.3 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.3 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.3 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.3 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.3 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.3 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## @backstage/plugin-auth-node@0.5.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog@1.22.0-next.3 + +### Patch Changes + +- 6582799: Add `tableOptions` to all tables and additionally `title` to API tables. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/catalog-client@1.6.6-next.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-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-catalog-backend@1.24.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + +## @backstage/plugin-catalog-backend-module-aws@0.3.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + +## @backstage/plugin-catalog-backend-module-azure@0.1.43-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.37-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.24-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.40-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-github@0.6.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-logs@0.0.2-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.31-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-catalog-common@1.0.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-catalog-graph@0.4.8-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + +## @backstage/plugin-catalog-node@1.12.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/plugin-catalog-react@1.12.3-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.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-common@1.0.26-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-devtools@0.1.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + +## @backstage/plugin-devtools-backend@0.3.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/plugin-events-backend@0.3.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-module-azure@0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-module-gerrit@0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-module-github@0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-module-gitlab@0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-backend-test-utils@0.1.33-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-events-node@0.3.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + +## @backstage/plugin-home@0.7.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + - @backstage/plugin-kubernetes-react@0.4.2-next.3 + +## @backstage/plugin-kubernetes-backend@0.18.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/plugin-kubernetes-cluster@0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + - @backstage/plugin-kubernetes-react@0.4.2-next.3 + +## @backstage/plugin-kubernetes-common@0.8.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-kubernetes-node@0.1.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + +## @backstage/plugin-kubernetes-react@0.4.2-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.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-kubernetes-common@0.8.2-next.2 + +## @backstage/plugin-notifications-backend@0.3.4-next.3 + +### Patch Changes + +- 7a05f50: Allow using notifications without users in the catalog +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## @backstage/plugin-notifications-backend-module-email@0.2.0-next.3 + +### Patch Changes + +- 83faf24: Notification email processor supports allowing or denying specific email addresses from receiving notifications +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + +## @backstage/plugin-notifications-node@0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## @backstage/plugin-org@0.6.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + +## @backstage/plugin-org-react@0.1.27-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.47-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## @backstage/plugin-permission-node@0.8.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-proxy-backend@0.5.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.3 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.3 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.3 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.3 + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.3 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.3 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.3 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.3 + +### Patch Changes + +- 187f583: Added examples for publish:azure action and updated its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.3 + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.3 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.3 + +### Patch Changes + +- d57967c: Add ability to set the initial commit message when initializing a repository using the scaffolder action. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.47-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.3 + +### Patch Changes + +- 6d4cb97: Added examples for github:repo:create action and improved test cases +- cd203f1: Added examples for action github:pages and improved its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.3 + +### Patch Changes + +- da97131: Added test cases for gitlab:issues:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.31-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.7-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.3 + +## @backstage/plugin-scaffolder-common@1.5.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-scaffolder-node@0.4.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-test-utils@0.4.5-next.3 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## @backstage/plugin-scaffolder-react@1.11.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + +## @backstage/plugin-search@1.4.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @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-common@1.2.14-next.1 + +## @backstage/plugin-search-backend@1.5.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/backend-openapi-utils@0.1.16-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-module-explore@0.1.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-module-pg@0.5.33-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-backend-node@1.2.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + +## @backstage/plugin-search-react@1.7.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @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-common@1.2.14-next.1 + +## @backstage/plugin-signals-backend@0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## @backstage/plugin-signals-node@0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## @backstage/plugin-techdocs@1.10.8-next.3 + +### Patch Changes + +- 27794d1: 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. +- 8543e72: TechDocs redirect feature now includes a notification to the user before they are redirected. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @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@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.37-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @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/test-utils@1.5.10-next.2 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## @backstage/plugin-techdocs-backend@1.10.10-next.3 + +### Patch Changes + +- a16632c: Update configuration schema to match actual behavior +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## @backstage/plugin-techdocs-node@1.12.9-next.3 + +### Patch Changes + +- 5cedd9f: Fix TechDocs Edit URL for nested docs +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-techdocs-react@1.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-user-settings@0.8.11-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @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.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.100-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.24.0-next.3 + - @backstage/plugin-notifications@0.3.0-next.1 + - @backstage/cli@0.27.0-next.4 + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-api-docs@0.11.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/frontend-app-api@0.7.5-next.3 + - @backstage/plugin-catalog-graph@0.4.8-next.4 + - @backstage/plugin-catalog-import@0.12.2-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-devtools@0.1.17-next.3 + - @backstage/plugin-home@0.7.9-next.3 + - @backstage/plugin-kubernetes@0.11.13-next.3 + - @backstage/plugin-org@0.6.28-next.3 + - @backstage/plugin-search@1.4.15-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/plugin-user-settings@0.8.11-next.3 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.3 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.1 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## example-app-next@0.0.14-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.24.0-next.3 + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/plugin-notifications@0.3.0-next.1 + - @backstage/cli@0.27.0-next.4 + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-api-docs@0.11.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/frontend-app-api@0.7.5-next.3 + - @backstage/plugin-app-visualizer@0.1.9-next.3 + - @backstage/plugin-catalog-graph@0.4.8-next.4 + - @backstage/plugin-catalog-import@0.12.2-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-home@0.7.9-next.3 + - @backstage/plugin-kubernetes@0.11.13-next.3 + - @backstage/plugin-org@0.6.28-next.3 + - @backstage/plugin-search@1.4.15-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/plugin-user-settings@0.8.11-next.3 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.3 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.1 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## app-next-example-plugin@0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-components@0.14.10-next.0 + +## example-backend@0.0.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.3 + - @backstage/plugin-notifications-backend@0.3.4-next.3 + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-techdocs-backend@1.10.10-next.3 + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/plugin-app-backend@0.3.72-next.3 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.3 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.3 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.3 + - @backstage/plugin-devtools-backend@0.3.9-next.3 + - @backstage/plugin-kubernetes-backend@0.18.4-next.3 + - @backstage/plugin-permission-backend@0.5.47-next.3 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-proxy-backend@0.5.4-next.3 + - @backstage/plugin-search-backend@1.5.15-next.3 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.3 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-signals-backend@0.1.9-next.3 + +## example-backend-legacy@0.2.101-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.3 + - @backstage/plugin-techdocs-backend@1.10.10-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-app-backend@0.3.72-next.3 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-devtools-backend@0.3.9-next.3 + - @backstage/plugin-events-backend@0.3.10-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-kubernetes-backend@0.18.4-next.3 + - @backstage/plugin-permission-backend@0.5.47-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-proxy-backend@0.5.4-next.3 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.3 + - @backstage/plugin-search-backend@1.5.15-next.3 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.3 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.3 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.3 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-signals-backend@0.1.9-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## e2e-test@0.2.19-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.4 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.4 + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## @internal/plugin-todo-list-backend@1.0.30-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 diff --git a/docs/tutorials/auth-service-migration.md b/docs/tutorials/auth-service-migration.md index 9e2633a12a..d12b0d5906 100644 --- a/docs/tutorials/auth-service-migration.md +++ b/docs/tutorials/auth-service-migration.md @@ -24,6 +24,8 @@ backend: dangerouslyDisableDefaultAuthPolicy: true ``` +Please note that this functionality will be removed in a future release, and you should migrate to using the new auth services as soon as possible or you would have to support your own service for issuing tokens. + In short, this will allow requests through to plugins in your backend, even if they do not include any credentials. The requests will still be treated as unauthenticated however, which not all plugin endpoints may accept. For more information on the impact of this configuration, see the [auth service documentation](../backend-system/core-services/auth.md). ### Migrating the backend diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index faaeab49f4..cba8ba7049 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -83,6 +83,28 @@ CMD ["node", "--require", "./instrumentation.js", "packages/backend", "--config" If you need to disable/configure some OpenTelemetry feature there are lots of [environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) which you can tweak. +### Available Metrics + +The following metrics are available: + +- `catalog_entities_count`: Total amount of entities in the catalog +- `catalog_registered_locations_count`: Total amount of registered locations in the catalog +- `catalog_relations_count`: Total amount of relations between entities +- `catalog.processed.entities.count`: Amount of entities processed +- `catalog.processing.duration`: Time spent executing the full processing flow +- `catalog.processors.duration`: Time spent executing catalog processors +- `catalog.processing.queue.delay`: The amount of delay between being scheduled for processing, and the start of actually being processed +- `catalog.stitched.entities.count`: Amount of entities stitched +- `catalog.stitching.duration`: Time spent executing the full stitching flow +- `catalog.stitching.queue.length`: Number of entities currently in the stitching queue +- `catalog.stitching.queue.delay`: The amount of delay between being scheduled for stitching, and the start of actually being stitched +- `scaffolder.task.count`: Count of task runs +- `scaffolder.task.duration`: Duration of a task run +- `scaffolder.step.count`: Count of step runs +- `scaffolder.step.duration`: Duration of a step runs +- `backend_tasks.task.runs.count`: Total number of times a task has been run +- `backend_tasks.task.runs.duration`: Histogram of task run durations + ## References - [Getting started with OpenTelemetry Node.js](https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/) 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/catalog-backend-module-gcp.yaml b/microsite/data/plugins/catalog-backend-module-gcp.yaml new file mode 100644 index 0000000000..b5df858862 --- /dev/null +++ b/microsite/data/plugins/catalog-backend-module-gcp.yaml @@ -0,0 +1,16 @@ +--- +title: GCP Entity Providers +author: BackToStage +authorUrl: https://backtostage.app/?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=catalog-backend-module-gcp +category: Catalog +description: Import your Infrastructure from GCP into Backstage as Resource Entities +documentation: https://github.com/backtostage/backstage-plugins/blob/main/plugins/catalog-backend-module-gcp/README.md +iconUrl: https://avatars1.githubusercontent.com/u/2810941?s=280&v=4 +npmPackageName: '@backtostage/plugin-catalog-backend-module-gcp' +tags: + - cloud + - project + - resources + - gcp + - cloudsql +addedDate: '2024-08-01' diff --git a/microsite/data/plugins/kubernetes-gpt-analyzer.yaml b/microsite/data/plugins/kubernetes-gpt-analyzer.yaml new file mode 100644 index 0000000000..78bd0fe4c4 --- /dev/null +++ b/microsite/data/plugins/kubernetes-gpt-analyzer.yaml @@ -0,0 +1,16 @@ +--- +title: VeeCode Kubernetes GPT Analyzer +author: VeeCode Platform +authorUrl: https://platform.vee.codes/ +category: Monitoring +description: The Kubernetes GPT Analyzer plug-in uses artificial intelligence with the help of k8s-operator to analyze and optimize your Kubernetes entities, improving the management and performance of your cluster. It makes it easier to detect anomalies and suggest best practices. +documentation: https://platform.vee.codes/plugin/Kubernetes%20GPT%20Analyzer/ +iconUrl: https://veecode-platform.github.io/support/imgs/logo_4.svg +npmPackageName: '@veecode-platform/backstage-plugin-kubernetes-gpt-analyzer' +tags: + - monitor + - ai + - kubernetes + - k8soperator + - gpt +addedDate: '2024-07-31' diff --git a/microsite/data/plugins/simple-icons.yaml b/microsite/data/plugins/simple-icons.yaml new file mode 100644 index 0000000000..45722ab421 --- /dev/null +++ b/microsite/data/plugins/simple-icons.yaml @@ -0,0 +1,10 @@ +--- +title: Simple Icons +author: dweber019 +authorUrl: https://github.com/dweber019 +category: Visualization +description: The Simple Icons plugin will add additional icons to be used as link icons. +documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/simple-icons +iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/simple-icons/docs/pluginIcon.png +npmPackageName: '@dweber019/backstage-plugin-simple-icons' +addedDate: '2024-07-30' 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..8874ede56e 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" ] }, { @@ -133,6 +134,7 @@ "features/search/concepts", "features/search/architecture", "features/search/search-engines", + "features/search/collators", "features/search/how-to-guides" ] }, @@ -296,6 +298,7 @@ "conf/writing", "conf/defining" ], + "Notifications": ["notifications/index"], "Auth and identity": [ "auth/index", { @@ -393,6 +396,7 @@ "backend-system/architecture/plugins", "backend-system/architecture/extension-points", "backend-system/architecture/modules", + "backend-system/architecture/feature-loaders", "backend-system/architecture/naming-patterns" ] }, @@ -449,13 +453,14 @@ "items": [ "frontend-system/architecture/index", "frontend-system/architecture/app", - "frontend-system/architecture/extensions", "frontend-system/architecture/plugins", + "frontend-system/architecture/extensions", + "frontend-system/architecture/extension-blueprints", "frontend-system/architecture/extension-overrides", + "frontend-system/architecture/references", "frontend-system/architecture/utility-apis", "frontend-system/architecture/routes", - "frontend-system/architecture/naming-patterns", - "frontend-system/architecture/references" + "frontend-system/architecture/naming-patterns" ] }, { diff --git a/mkdocs.yml b/mkdocs.yml index 8527d68c28..33c9af3286 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,12 +64,14 @@ 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' - Concepts: 'features/search/concepts.md' - Search Architecture: 'features/search/architecture.md' - Search Engines: 'features/search/search-engines.md' + - Collators: 'features/search/collators.md' - HOW TO guides: 'features/search/how-to-guides.md' - TechDocs: - Overview: 'features/techdocs/README.md' diff --git a/package.json b/package.json index 8e7a047d7b..285d3ad3f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.30.0-next.2", + "version": "1.30.0-next.4", "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..58762ab5e5 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,21 @@ # app-next-example-plugin +## 0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-components@0.14.10-next.0 + +## 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..3c8b740e1c 100644 --- a/packages/app-next-example-plugin/api-report.md +++ b/packages/app-next-example-plugin/api-report.md @@ -3,11 +3,45 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) -const examplePlugin: BackstagePlugin<{}, {}>; +const examplePlugin: BackstagePlugin< + {}, + {}, + { + 'page:example': ExtensionDefinition< + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + undefined + >; + } +>; 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..e3ddfe32dd 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.3", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next-example-plugin/src/plugin.tsx b/packages/app-next-example-plugin/src/plugin.tsx index 99c377b7e5..818e8abc84 100644 --- a/packages/app-next-example-plugin/src/plugin.tsx +++ b/packages/app-next-example-plugin/src/plugin.tsx @@ -16,17 +16,19 @@ import React from 'react'; import { - createPageExtension, - createPlugin, + PageBlueprint, + createFrontendPlugin, } from '@backstage/frontend-plugin-api'; -export const ExamplePage = createPageExtension({ - defaultPath: '/example', - loader: () => import('./Component').then(m => ), +export const ExamplePage = PageBlueprint.make({ + params: { + defaultPath: '/example', + loader: () => import('./Component').then(m => ), + }, }); /** @public */ -export const examplePlugin = createPlugin({ +export const examplePlugin = createFrontendPlugin({ id: 'example', extensions: [ExamplePage], }); diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 7eb9535771..f37f968fed 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,91 @@ # example-app-next +## 0.0.14-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.24.0-next.3 + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/plugin-notifications@0.3.0-next.1 + - @backstage/cli@0.27.0-next.4 + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-api-docs@0.11.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/frontend-app-api@0.7.5-next.3 + - @backstage/plugin-app-visualizer@0.1.9-next.3 + - @backstage/plugin-catalog-graph@0.4.8-next.4 + - @backstage/plugin-catalog-import@0.12.2-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-home@0.7.9-next.3 + - @backstage/plugin-kubernetes@0.11.13-next.3 + - @backstage/plugin-org@0.6.28-next.3 + - @backstage/plugin-search@1.4.15-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/plugin-user-settings@0.8.11-next.3 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.3 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.1 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## 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..e8905091bb 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.4", "private": true, "repository": { "type": "git", diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index b57c680474..c41f33e01b 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -26,8 +26,8 @@ import homePlugin, { import { coreExtensionData, createExtension, - createApiExtension, createExtensionOverrides, + ApiBlueprint, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; import appVisualizerPlugin from '@backstage/plugin-app-visualizer'; @@ -40,6 +40,7 @@ import { createApiFactory, configApiRef } from '@backstage/core-plugin-api'; import { ScmAuth, ScmIntegrationsApi, + scmAuthApiRef, scmIntegrationsApiRef, } from '@backstage/integration-react'; import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha'; @@ -77,25 +78,31 @@ TODO: const homePageExtension = createExtension({ name: 'myhomepage', attachTo: { id: 'page:home', input: 'props' }, - output: { - children: coreExtensionData.reactElement, - title: titleExtensionDataRef, - }, + output: [coreExtensionData.reactElement, titleExtensionDataRef], factory() { - return { children: homePage, title: 'just a title' }; + return [ + coreExtensionData.reactElement(homePage), + titleExtensionDataRef('just a title'), + ]; }, }); -const scmAuthExtension = createApiExtension({ - factory: ScmAuth.createDefaultApiFactory(), +const scmAuthExtension = ApiBlueprint.make({ + namespace: scmAuthApiRef.id, + params: { + factory: ScmAuth.createDefaultApiFactory(), + }, }); -const scmIntegrationApi = createApiExtension({ - factory: createApiFactory({ - api: scmIntegrationsApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), - }), +const scmIntegrationApi = ApiBlueprint.make({ + namespace: scmIntegrationsApiRef.id, + params: { + factory: createApiFactory({ + api: scmIntegrationsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), + }), + }, }); const collectedLegacyPlugins = convertLegacyApp( diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 826040a6cb..8eca7d1e6c 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -17,11 +17,11 @@ import React from 'react'; import { Link } from '@backstage/core-components'; import { - createPageExtension, - createPlugin, + createFrontendPlugin, createRouteRef, createExternalRouteRef, useRouteRef, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import { Route, Routes } from 'react-router-dom'; @@ -37,93 +37,101 @@ export const pageXRouteRef = createRouteRef(); // path: '/page2', // }); -const IndexPage = createPageExtension({ +const IndexPage = PageBlueprint.make({ name: 'index', - defaultPath: '/', - routeRef: indexRouteRef, - loader: async () => { - const Component = () => { - const page1Link = useRouteRef(page1RouteRef); - return ( -
- op + params: { + defaultPath: '/', + routeRef: indexRouteRef, + loader: async () => { + const Component = () => { + const page1Link = useRouteRef(page1RouteRef); + return (
- Page 1 -
-
- Home -
-
- GraphiQL -
-
- Search -
-
- Settings -
-
- ); - }; - return ; - }, -}); - -const Page1 = createPageExtension({ - name: 'page1', - defaultPath: '/page1', - routeRef: page1RouteRef, - loader: async () => { - const Component = () => { - const indexLink = useRouteRef(indexRouteRef); - const xLink = useRouteRef(externalPageXRouteRef); - // const page2Link = useRouteRef(page2RouteRef); - - return ( -
-

This is page 1

- Go back - Page 2 - {/* Page 2 */} - Page X - -
- Sub-page content: + op + {page1Link && ( +
+ Page 1 +
+ )}
- - This is also page 1} /> - This is page 2} /> - + Home +
+
+ GraphiQL +
+
+ Search +
+
+ Settings
-
- ); - }; - return ; + ); + }; + return ; + }, }, }); -const ExternalPage = createPageExtension({ +const Page1 = PageBlueprint.make({ + name: 'page1', + params: { + defaultPath: '/page1', + routeRef: page1RouteRef, + loader: async () => { + const Component = () => { + const indexLink = useRouteRef(indexRouteRef); + const xLink = useRouteRef(externalPageXRouteRef); + // const page2Link = useRouteRef(page2RouteRef); + + return ( +
+

This is page 1

+ {indexLink && Go back} + Page 2 + {/* Page 2 */} + {xLink && Page X} + +
+ Sub-page content: +
+ + This is also page 1} /> + This is page 2} /> + +
+
+
+ ); + }; + return ; + }, + }, +}); + +const ExternalPage = PageBlueprint.make({ name: 'pageX', - defaultPath: '/pageX', - routeRef: pageXRouteRef, - loader: async () => { - const Component = () => { - const indexLink = useRouteRef(indexRouteRef); - // const pageXLink = useRouteRef(pageXRouteRef); + params: { + defaultPath: '/pageX', + routeRef: pageXRouteRef, + loader: async () => { + const Component = () => { + const indexLink = useRouteRef(indexRouteRef); + // const pageXLink = useRouteRef(pageXRouteRef); - return ( -
-

This is page X

- Go back -
- ); - }; - return ; + return ( +
+

This is page X

+ {indexLink && Go back} +
+ ); + }; + return ; + }, }, }); -export const pagesPlugin = createPlugin({ +export const pagesPlugin = createFrontendPlugin({ id: 'pages', // routes: { // index: indexRouteRef, diff --git a/packages/app-next/src/overrides/SignInPage.tsx b/packages/app-next/src/overrides/SignInPage.tsx index 364a749293..52dd01fcde 100644 --- a/packages/app-next/src/overrides/SignInPage.tsx +++ b/packages/app-next/src/overrides/SignInPage.tsx @@ -17,13 +17,16 @@ import React from 'react'; import { SignInPage } from '@backstage/core-components'; import { + SignInPageBlueprint, createExtensionOverrides, - createSignInPageExtension, } from '@backstage/frontend-plugin-api'; -const signInPage = createSignInPageExtension({ +const signInPage = SignInPageBlueprint.make({ name: 'guest', - loader: async () => props => , + params: { + loader: async () => props => + , + }, }); export const signInPageOverrides = createExtensionOverrides({ diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 4bcd5461a7..c80450daa9 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,87 @@ # example-app +## 0.2.100-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.24.0-next.3 + - @backstage/plugin-notifications@0.3.0-next.1 + - @backstage/cli@0.27.0-next.4 + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-api-docs@0.11.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/frontend-app-api@0.7.5-next.3 + - @backstage/plugin-catalog-graph@0.4.8-next.4 + - @backstage/plugin-catalog-import@0.12.2-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-devtools@0.1.17-next.3 + - @backstage/plugin-home@0.7.9-next.3 + - @backstage/plugin-kubernetes@0.11.13-next.3 + - @backstage/plugin-org@0.6.28-next.3 + - @backstage/plugin-search@1.4.15-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/plugin-user-settings@0.8.11-next.3 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.3 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.1 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## 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..14fc3a0ae6 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.4", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index cbbb9e949e..5d8507cbd2 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/backend-app-api +## 0.8.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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..c7b47cb1a0 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -3,54 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - -import type { AppConfig } from '@backstage/config'; -import { AuthService } from '@backstage/backend-plugin-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; -import { CacheService } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; -import { ConfigSchema } from '@backstage/config-loader'; -import { CorsOptions } from 'cors'; -import { DatabaseService } from '@backstage/backend-plugin-api'; -import { DiscoveryService } from '@backstage/backend-plugin-api'; -import { ErrorRequestHandler } from 'express'; -import { Express as Express_2 } from 'express'; -import { Format } from 'logform'; -import { Handler } from 'express'; -import { HelmetOptions } from 'helmet'; -import * as http from 'http'; -import { HttpAuthService } from '@backstage/backend-plugin-api'; -import { HttpRouterService } from '@backstage/backend-plugin-api'; -import { HumanDuration } from '@backstage/types'; import { IdentityService } from '@backstage/backend-plugin-api'; -import { JsonObject } from '@backstage/types'; -import { LifecycleService } from '@backstage/backend-plugin-api'; -import { LoadConfigOptionsRemote } from '@backstage/config-loader'; -import { LoggerService } from '@backstage/backend-plugin-api'; -import { PermissionsService } from '@backstage/backend-plugin-api'; -import { RemoteConfigSourceOptions } from '@backstage/config-loader'; -import { RequestHandler } from 'express'; -import { RequestListener } from 'http'; -import { RootConfigService } from '@backstage/backend-plugin-api'; -import { RootHttpRouterService } from '@backstage/backend-plugin-api'; -import { RootLifecycleService } from '@backstage/backend-plugin-api'; -import { RootLoggerService } from '@backstage/backend-plugin-api'; -import { SchedulerService } from '@backstage/backend-plugin-api'; -import type { Server } from 'node:http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; -import { transport } from 'winston'; -import { UrlReaderService } from '@backstage/backend-plugin-api'; -import { UserInfoService } from '@backstage/backend-plugin-api'; - -// @public @deprecated (undocumented) -export const authServiceFactory: ServiceFactoryCompat< - AuthService, - 'plugin', - undefined ->; // @public (undocumented) export interface Backend { @@ -62,42 +19,12 @@ export interface Backend { default: BackendFeature; }>, ): void; - // @deprecated (undocumented) - add( - feature: - | (() => BackendFeature) - | Promise<{ - default: () => BackendFeature; - }>, - ): void; // (undocumented) start(): Promise; // (undocumented) stop(): Promise; } -// @public @deprecated (undocumented) -export const cacheServiceFactory: ServiceFactoryCompat< - CacheService, - 'plugin', - undefined ->; - -// Warning: (ae-forgotten-export) The symbol "createConfigSecretEnumerator_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export const createConfigSecretEnumerator: typeof createConfigSecretEnumerator_2; - -// Warning: (ae-forgotten-export) The symbol "createHttpServer_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export const createHttpServer: typeof createHttpServer_2; - -// Warning: (ae-forgotten-export) The symbol "createLifecycleMiddleware_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated -export const createLifecycleMiddleware: typeof createLifecycleMiddleware_2; - // @public (undocumented) export function createSpecializedBackend( options: CreateSpecializedBackendOptions, @@ -109,78 +36,6 @@ export interface CreateSpecializedBackendOptions { defaultServiceFactories: ServiceFactory[]; } -// @public @deprecated (undocumented) -export const databaseServiceFactory: ServiceFactoryCompat< - DatabaseService, - 'plugin', - undefined ->; - -// @public @deprecated -export class DefaultRootHttpRouter implements RootHttpRouterService { - // (undocumented) - static create(options?: DefaultRootHttpRouterOptions): DefaultRootHttpRouter; - // (undocumented) - handler(): Handler; - // (undocumented) - use(path: string, handler: Handler): void; -} - -// Warning: (ae-forgotten-export) The symbol "DefaultRootHttpRouterOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated -export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; - -// @public @deprecated (undocumented) -export const discoveryServiceFactory: ServiceFactoryCompat< - DiscoveryService, - 'plugin', - undefined ->; - -// Warning: (ae-forgotten-export) The symbol "ExtendedHttpServer_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type ExtendedHttpServer = ExtendedHttpServer_2; - -// @public @deprecated -export class HostDiscovery implements DiscoveryService { - static fromConfig( - config: Config, - options?: { - basePath?: string; - }, - ): HostDiscovery; - // (undocumented) - getBaseUrl(pluginId: string): Promise; - // (undocumented) - getExternalBaseUrl(pluginId: string): Promise; -} - -// @public @deprecated (undocumented) -export const httpAuthServiceFactory: ServiceFactoryCompat< - HttpAuthService, - 'plugin', - undefined ->; - -// @public @deprecated -export const httpRouterServiceFactory: ServiceFactoryCompat< - HttpRouterService, - 'plugin', - undefined ->; - -// Warning: (ae-forgotten-export) The symbol "HttpServerCertificateOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type HttpServerCertificateOptions = HttpServerCertificateOptions_2; - -// Warning: (ae-forgotten-export) The symbol "HttpServerOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type HttpServerOptions = HttpServerOptions_2; - // @public @deprecated export type IdentityFactoryOptions = { issuer?: string; @@ -191,178 +46,15 @@ export type IdentityFactoryOptions = { export const identityServiceFactory: ServiceFactoryCompat< IdentityService, 'plugin', + 'singleton', IdentityFactoryOptions >; -// Warning: (ae-forgotten-export) The symbol "LifecycleMiddlewareOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated -export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; - -// @public @deprecated -export const lifecycleServiceFactory: ServiceFactoryCompat< - LifecycleService, - 'plugin', - undefined ->; - -// @public @deprecated -export function loadBackendConfig(options: { - remote?: LoadConfigOptionsRemote; - argv: string[]; - additionalConfigs?: AppConfig[]; - watch?: boolean; -}): Promise<{ - config: Config; -}>; - -// @public @deprecated -export const loggerServiceFactory: ServiceFactoryCompat< - LoggerService, - 'plugin', - undefined ->; - -// @public @deprecated (undocumented) -export class MiddlewareFactory { - compression(): RequestHandler; - cors(): RequestHandler; - static create(options: MiddlewareFactoryOptions): MiddlewareFactory; - error(options?: MiddlewareFactoryErrorOptions): ErrorRequestHandler; - helmet(): RequestHandler; - logging(): RequestHandler; - notFound(): RequestHandler; -} - -// Warning: (ae-forgotten-export) The symbol "MiddlewareFactoryErrorOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type MiddlewareFactoryErrorOptions = MiddlewareFactoryErrorOptions_2; - -// Warning: (ae-forgotten-export) The symbol "MiddlewareFactoryOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; - -// @public @deprecated (undocumented) -export const permissionsServiceFactory: ServiceFactoryCompat< - PermissionsService, - 'plugin', - undefined ->; - -// Warning: (ae-forgotten-export) The symbol "readCorsOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export const readCorsOptions: typeof readCorsOptions_2; - -// Warning: (ae-forgotten-export) The symbol "readHelmetOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export const readHelmetOptions: typeof readHelmetOptions_2; - -// Warning: (ae-forgotten-export) The symbol "readHttpServerOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export const readHttpServerOptions: typeof readHttpServerOptions_2; - -// @public @deprecated (undocumented) -export interface RootConfigFactoryOptions { - argv?: string[]; - remote?: Pick; - // (undocumented) - watch?: boolean; -} - -// @public @deprecated (undocumented) -export const rootConfigServiceFactory: ServiceFactoryCompat< - RootConfigService, - 'root', - RootConfigFactoryOptions ->; - -// Warning: (ae-forgotten-export) The symbol "RootHttpRouterConfigureContext_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type RootHttpRouterConfigureContext = RootHttpRouterConfigureContext_2; - -// Warning: (ae-forgotten-export) The symbol "RootHttpRouterFactoryOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated -export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; - -// @public @deprecated (undocumented) -export const rootHttpRouterServiceFactory: (( - options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory) & - ServiceFactory; - -// @public @deprecated -export const rootLifecycleServiceFactory: ServiceFactoryCompat< - RootLifecycleService, - 'root', - undefined ->; - -// @public @deprecated -export const rootLoggerServiceFactory: ServiceFactoryCompat< - RootLoggerService, - 'root', - undefined ->; - -// @public @deprecated (undocumented) -export const schedulerServiceFactory: ServiceFactoryCompat< - SchedulerService, - 'plugin', - undefined ->; - // @public @deprecated (undocumented) export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, 'plugin', + 'singleton', undefined >; - -// @public @deprecated (undocumented) -export const urlReaderServiceFactory: ServiceFactoryCompat< - UrlReaderService, - 'plugin', - undefined ->; - -// @public @deprecated (undocumented) -export const userInfoServiceFactory: ServiceFactoryCompat< - UserInfoService, - 'plugin', - undefined ->; - -// @public @deprecated -export class WinstonLogger implements RootLoggerService { - // (undocumented) - addRedactions(redactions: Iterable): void; - // (undocumented) - child(meta: JsonObject): LoggerService; - static colorFormat(): Format; - static create(options: WinstonLoggerOptions): WinstonLogger; - // (undocumented) - debug(message: string, meta?: JsonObject): void; - // (undocumented) - error(message: string, meta?: JsonObject): void; - // (undocumented) - info(message: string, meta?: JsonObject): void; - static redacter(): { - format: Format; - add: (redactions: Iterable) => void; - }; - // (undocumented) - warn(message: string, meta?: JsonObject): void; -} - -// Warning: (ae-forgotten-export) The symbol "WinstonLoggerOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type WinstonLoggerOptions = WinstonLoggerOptions_2; ``` diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 6a4e3a669b..588a969390 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.3", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" @@ -62,8 +62,6 @@ "@backstage/plugin-permission-node": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@types/cors": "^2.8.6", - "@types/express": "^4.17.6", "compression": "^1.7.4", "cookie": "^0.6.0", "cors": "^2.8.5", diff --git a/packages/backend-app-api/src/http/index.ts b/packages/backend-app-api/src/http/index.ts deleted file mode 100644 index a056ed2d61..0000000000 --- a/packages/backend-app-api/src/http/index.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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 { ErrorRequestHandler, RequestHandler } from 'express'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - readHttpServerOptions as _readHttpServerOptions, - createHttpServer as _createHttpServer, - MiddlewareFactory as _MiddlewareFactory, - readCorsOptions as _readCorsOptions, - readHelmetOptions as _readHelmetOptions, - type MiddlewareFactoryErrorOptions as _MiddlewareFactoryErrorOptions, - type MiddlewareFactoryOptions as _MiddlewareFactoryOptions, - type ExtendedHttpServer as _ExtendedHttpServer, - type HttpServerCertificateOptions as _HttpServerCertificateOptions, - type HttpServerOptions as _HttpServerOptions, -} from '../../../backend-defaults/src/entrypoints/rootHttpRouter/http'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export const readHttpServerOptions = _readHttpServerOptions; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export const createHttpServer = _createHttpServer; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export const readCorsOptions = _readCorsOptions; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export const readHelmetOptions = _readHelmetOptions; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export class MiddlewareFactory { - /** - * Creates a new {@link MiddlewareFactory}. - */ - static create(options: MiddlewareFactoryOptions) { - return new MiddlewareFactory(_MiddlewareFactory.create(options)); - } - - private constructor(private readonly impl: _MiddlewareFactory) {} - - /** - * Returns a middleware that unconditionally produces a 404 error response. - * - * @remarks - * - * Typically you want to place this middleware at the end of the chain, such - * that it's the last one attempted after no other routes matched. - * - * @returns An Express request handler - */ - notFound(): RequestHandler { - return this.impl.notFound(); - } - - /** - * Returns the compression middleware. - * - * @remarks - * - * The middleware will attempt to compress response bodies for all requests - * that traverse through the middleware. - */ - compression(): RequestHandler { - return this.impl.compression(); - } - - /** - * Returns a request logging middleware. - * - * @remarks - * - * Typically you want to place this middleware at the start of the chain, such - * that it always logs requests whether they are "caught" by handlers farther - * down or not. - * - * @returns An Express request handler - */ - logging(): RequestHandler { - return this.impl.logging(); - } - - /** - * Returns a middleware that implements the helmet library. - * - * @remarks - * - * This middleware applies security policies to incoming requests and outgoing - * responses. It is configured using config keys such as `backend.csp`. - * - * @see {@link https://helmetjs.github.io/} - * - * @returns An Express request handler - */ - helmet(): RequestHandler { - return this.impl.helmet(); - } - - /** - * Returns a middleware that implements the cors library. - * - * @remarks - * - * This middleware handles CORS. It is configured using the config key - * `backend.cors`. - * - * @see {@link https://github.com/expressjs/cors} - * - * @returns An Express request handler - */ - cors(): RequestHandler { - return this.impl.cors(); - } - - /** - * Express middleware to handle errors during request processing. - * - * @remarks - * - * This is commonly the very last middleware in the chain. - * - * Its primary purpose is not to do translation of business logic exceptions, - * but rather to be a global catch-all for uncaught "fatal" errors that are - * expected to result in a 500 error. However, it also does handle some common - * error types (such as http-error exceptions, and the well-known error types - * in the `@backstage/errors` package) and returns the enclosed status code - * accordingly. - * - * It will also produce a response body with a serialized form of the error, - * unless a previous handler already did send a body. See - * {@link @backstage/errors#ErrorResponseBody} for the response shape used. - * - * @returns An Express error request handler - */ - error(options: MiddlewareFactoryErrorOptions = {}): ErrorRequestHandler { - return this.impl.error(options); - } -} -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type MiddlewareFactoryErrorOptions = _MiddlewareFactoryErrorOptions; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type MiddlewareFactoryOptions = _MiddlewareFactoryOptions; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type ExtendedHttpServer = _ExtendedHttpServer; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type HttpServerCertificateOptions = _HttpServerCertificateOptions; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type HttpServerOptions = _HttpServerOptions; diff --git a/packages/backend-app-api/src/index.ts b/packages/backend-app-api/src/index.ts index 13864a3470..02633f3732 100644 --- a/packages/backend-app-api/src/index.ts +++ b/packages/backend-app-api/src/index.ts @@ -20,8 +20,5 @@ * @packageDocumentation */ -export * from './config'; -export * from './http'; -export * from './logging'; export * from './wiring'; export * from './services/implementations'; diff --git a/packages/backend-app-api/src/logging/WinstonLogger.ts b/packages/backend-app-api/src/logging/WinstonLogger.ts deleted file mode 100644 index e5403906ce..0000000000 --- a/packages/backend-app-api/src/logging/WinstonLogger.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - WinstonLogger as _WinstonLogger, - type WinstonLoggerOptions as _WinstonLoggerOptions, -} from '../../../backend-defaults/src/entrypoints/rootLogger'; - -import { - LoggerService, - RootLoggerService, -} from '@backstage/backend-plugin-api'; -import { JsonObject } from '@backstage/types'; -import { Format } from 'logform'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootLogger` instead. - */ -export type WinstonLoggerOptions = _WinstonLoggerOptions; - -/** - * A {@link @backstage/backend-plugin-api#LoggerService} implementation based on winston. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootLogger` instead. - */ -export class WinstonLogger implements RootLoggerService { - /** - * Creates a {@link WinstonLogger} instance. - */ - static create(options: WinstonLoggerOptions): WinstonLogger { - return new WinstonLogger(_WinstonLogger.create(options)); - } - - /** - * Creates a winston log formatter for redacting secrets. - */ - static redacter(): { - format: Format; - add: (redactions: Iterable) => void; - } { - return _WinstonLogger.redacter(); - } - - /** - * Creates a pretty printed winston log formatter. - */ - static colorFormat(): Format { - return _WinstonLogger.colorFormat(); - } - - private constructor(private readonly impl: _WinstonLogger) {} - - error(message: string, meta?: JsonObject): void { - this.impl.error(message, meta); - } - - warn(message: string, meta?: JsonObject): void { - this.impl.warn(message, meta); - } - - info(message: string, meta?: JsonObject): void { - this.impl.info(message, meta); - } - - debug(message: string, meta?: JsonObject): void { - this.impl.debug(message, meta); - } - - child(meta: JsonObject): LoggerService { - return this.impl.child(meta); - } - - addRedactions(redactions: Iterable) { - this.impl.addRedactions(redactions); - } -} diff --git a/packages/backend-app-api/src/logging/index.ts b/packages/backend-app-api/src/logging/index.ts deleted file mode 100644 index 14fe33f898..0000000000 --- a/packages/backend-app-api/src/logging/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -export { WinstonLogger } from './WinstonLogger'; -export type { WinstonLoggerOptions } from './WinstonLogger'; diff --git a/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts deleted file mode 100644 index 9b5e5ab16a..0000000000 --- a/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts +++ /dev/null @@ -1,40 +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 { CacheManager } from '@backstage/backend-common'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/cache` instead. - */ -export const cacheServiceFactory = createServiceFactory({ - service: coreServices.cache, - deps: { - config: coreServices.rootConfig, - logger: coreServices.rootLogger, - plugin: coreServices.pluginMetadata, - }, - async createRootContext({ config, logger }) { - return CacheManager.fromConfig(config, { logger }); - }, - async factory({ plugin }, manager) { - return manager.forPlugin(plugin.getId()).getClient(); - }, -}); diff --git a/packages/backend-app-api/src/services/implementations/config/index.ts b/packages/backend-app-api/src/services/implementations/config/index.ts deleted file mode 100644 index 1775ef2efc..0000000000 --- a/packages/backend-app-api/src/services/implementations/config/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -export { rootConfigServiceFactory } from './rootConfigServiceFactory'; -export type { RootConfigFactoryOptions } from './rootConfigServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts b/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts deleted file mode 100644 index c74474e629..0000000000 --- a/packages/backend-app-api/src/services/implementations/config/rootConfigServiceFactory.ts +++ /dev/null @@ -1,61 +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 { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - ConfigSources, - RemoteConfigSourceOptions, -} from '@backstage/config-loader'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead. - */ -export interface RootConfigFactoryOptions { - /** - * Process arguments to use instead of the default `process.argv()`. - */ - argv?: string[]; - - /** - * Enables and sets options for remote configuration loading. - */ - remote?: Pick; - watch?: boolean; -} - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead. - */ -export const rootConfigServiceFactory = createServiceFactory( - (options?: RootConfigFactoryOptions) => ({ - service: coreServices.rootConfig, - deps: {}, - async factory() { - const source = ConfigSources.default({ - argv: options?.argv, - remote: options?.remote, - watch: options?.watch, - }); - console.log(`Loading config from ${source}`); - return await ConfigSources.toConfig(source); - }, - }), -); diff --git a/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts deleted file mode 100644 index 972d8dd4ec..0000000000 --- a/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts +++ /dev/null @@ -1,52 +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 { DatabaseManager } from '@backstage/backend-common'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { ConfigReader } from '@backstage/config'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/database` instead. - */ -export const databaseServiceFactory = createServiceFactory({ - service: coreServices.database, - deps: { - config: coreServices.rootConfig, - lifecycle: coreServices.lifecycle, - pluginMetadata: coreServices.pluginMetadata, - }, - async createRootContext({ config }) { - return config.getOptional('backend.database') - ? DatabaseManager.fromConfig(config) - : DatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { client: 'better-sqlite3', connection: ':memory:' }, - }, - }), - ); - }, - async factory({ pluginMetadata, lifecycle }, databaseManager) { - return databaseManager.forPlugin(pluginMetadata.getId(), { - pluginMetadata, - lifecycle, - }); - }, -}); diff --git a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.test.ts b/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.test.ts deleted file mode 100644 index 4e6aff5853..0000000000 --- a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* - * 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 { ConfigReader } from '@backstage/config'; -import { HostDiscovery } from './HostDiscovery'; - -describe('HostDiscovery', () => { - it('is created from config', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - }), - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - 'http://localhost:80/api/catalog', - ); - await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( - 'http://localhost:40/api/catalog', - ); - }); - - it('strips trailing slashes in config', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40//', - listen: { port: 80, host: 'localhost' }, - }, - }), - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - 'http://localhost:80/api/catalog', - ); - await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( - 'http://localhost:40/api/catalog', - ); - }); - - it('can configure the base path', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - }), - { basePath: '/service' }, - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - 'http://localhost:80/service/catalog', - ); - await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( - 'http://localhost:40/service/catalog', - ); - }); - - it.each([ - [{ listen: ':80' }, 'http://localhost:80'], - [{ listen: ':40', https: true }, 'https://localhost:40'], - [{ listen: '127.0.0.1:80' }, 'http://127.0.0.1:80'], - [{ listen: '127.0.0.1:80', https: true }, 'https://127.0.0.1:80'], - [{ listen: '0.0.0.0:40' }, 'http://127.0.0.1:40'], - [{ listen: { port: 80 } }, 'http://localhost:80'], - [{ listen: { port: 8000 } }, 'http://localhost:8000'], - [{ listen: { port: 80, host: '0.0.0.0' } }, 'http://127.0.0.1:80'], - [{ listen: { port: 80, host: '::' } }, 'http://localhost:80'], - [{ listen: { port: 80, host: '::1' } }, 'http://[::1]:80'], - [{ listen: { port: 90, host: '::2' }, https: true }, 'https://[::2]:90'], - ])('resolves internal baseUrl for %j as %s', async (config, expected) => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - ...config, - }, - }), - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - `${expected}/api/catalog`, - ); - }); - - it('uses plugin specific targets from config if provided', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - discovery: { - endpoints: [ - { - target: { - internal: 'http://catalog-backend-internal:8080/api/catalog', - external: 'http://catalog-backend-external:8080/api/catalog', - }, - plugins: ['catalog'], - }, - ], - }, - }), - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - 'http://catalog-backend-internal:8080/api/catalog', - ); - await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( - 'http://catalog-backend-external:8080/api/catalog', - ); - }); - - it('uses a single target for internal and external for a plugin', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - discovery: { - endpoints: [ - { - target: 'http://catalog-backend:8080/api/catalog', - plugins: ['catalog'], - }, - ], - }, - }), - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - 'http://catalog-backend:8080/api/catalog', - ); - await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( - 'http://catalog-backend:8080/api/catalog', - ); - }); - - it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - discovery: { - endpoints: [ - { - target: 'http://catalog-backend:8080/api/catalog', - plugins: ['catalog'], - }, - ], - }, - }), - ); - - await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe( - 'http://localhost:80/api/scaffolder', - ); - await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe( - 'http://localhost:40/api/scaffolder', - ); - }); - - it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - discovery: { - endpoints: [ - { - target: 'http://common-backend:8080/api/{{pluginId}}', - plugins: ['catalog', 'docs'], - }, - { - target: { - internal: 'http://scaffolder-internal:8080/api/{{ pluginId }}', - external: 'http://scaffolder-external:8080/api/{{ pluginId }}', - }, - plugins: ['scaffolder'], - }, - ], - }, - }), - ); - - await expect(discovery.getBaseUrl('catalog')).resolves.toBe( - 'http://common-backend:8080/api/catalog', - ); - await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe( - 'http://common-backend:8080/api/catalog', - ); - await expect(discovery.getBaseUrl('docs')).resolves.toBe( - 'http://common-backend:8080/api/docs', - ); - await expect(discovery.getExternalBaseUrl('docs')).resolves.toBe( - 'http://common-backend:8080/api/docs', - ); - await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe( - 'http://scaffolder-internal:8080/api/scaffolder', - ); - await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe( - 'http://scaffolder-external:8080/api/scaffolder', - ); - }); - - it('encodes the pluginId', async () => { - const discovery = HostDiscovery.fromConfig( - new ConfigReader({ - backend: { - baseUrl: 'http://localhost:40', - listen: { port: 80, host: 'localhost' }, - }, - discovery: { - endpoints: [ - { - target: 'http://common-backend:8080/api/{{pluginId}}', - plugins: ['plugin/beta'], - }, - ], - }, - }), - ); - - await expect(discovery.getBaseUrl('plugin/beta')).resolves.toBe( - 'http://common-backend:8080/api/plugin%2Fbeta', - ); - await expect(discovery.getBaseUrl('plugin/alpha')).resolves.toBe( - 'http://localhost:80/api/plugin%2Falpha', - ); - await expect(discovery.getExternalBaseUrl('plugin/alpha')).resolves.toBe( - 'http://localhost:40/api/plugin%2Falpha', - ); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts b/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts deleted file mode 100644 index 5909dd1ae6..0000000000 --- a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { Config } from '@backstage/config'; -import { DiscoveryService } from '@backstage/backend-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { HostDiscovery as _HostDiscovery } from '../../../../../backend-defaults/src/entrypoints/discovery'; - -/** - * HostDiscovery is a basic PluginEndpointDiscovery implementation - * that can handle plugins that are hosted in a single or multiple deployments. - * - * The deployment may be scaled horizontally, as long as the external URL - * is the same for all instances. However, internal URLs will always be - * resolved to the same host, so there won't be any balancing of internal traffic. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/discovery` instead. - */ -export class HostDiscovery implements DiscoveryService { - /** - * Creates a new HostDiscovery discovery instance by reading - * from the `backend` config section, specifically the `.baseUrl` for - * discovering the external URL, and the `.listen` and `.https` config - * for the internal one. - * - * Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`. - * eg. - * ```yaml - * discovery: - * endpoints: - * - target: https://internal.example.com/internal-catalog - * plugins: [catalog] - * - target: https://internal.example.com/secure/api/{{pluginId}} - * plugins: [auth, permission] - * - target: - * internal: https://internal.example.com/search - * external: https://example.com/search - * plugins: [search] - * ``` - * - * The basePath defaults to `/api`, meaning the default full internal - * path for the `catalog` plugin will be `http://localhost:7007/api/catalog`. - */ - static fromConfig(config: Config, options?: { basePath?: string }) { - return new HostDiscovery(_HostDiscovery.fromConfig(config, options)); - } - - private constructor(private readonly impl: _HostDiscovery) {} - - async getBaseUrl(pluginId: string): Promise { - return this.impl.getBaseUrl(pluginId); - } - - async getExternalBaseUrl(pluginId: string): Promise { - return this.impl.getExternalBaseUrl(pluginId); - } -} diff --git a/packages/backend-app-api/src/services/implementations/discovery/index.ts b/packages/backend-app-api/src/services/implementations/discovery/index.ts deleted file mode 100644 index ee4851271a..0000000000 --- a/packages/backend-app-api/src/services/implementations/discovery/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -export { discoveryServiceFactory } from './discoveryServiceFactory'; -export { HostDiscovery } from './HostDiscovery'; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts deleted file mode 100644 index d4864a23a6..0000000000 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts +++ /dev/null @@ -1,45 +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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - createLifecycleMiddleware as _createLifecycleMiddleware, - type LifecycleMiddlewareOptions as _LifecycleMiddlewareOptions, -} from '../../../../../backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware'; - -/** - * Options for {@link createLifecycleMiddleware}. - * @public - * @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead. - */ -export type LifecycleMiddlewareOptions = _LifecycleMiddlewareOptions; - -/** - * Creates a middleware that pauses requests until the service has started. - * - * @remarks - * - * Requests that arrive before the service has started will be paused until startup is complete. - * If the service does not start within the provided timeout, the request will be rejected with a - * {@link @backstage/errors#ServiceUnavailableError}. - * - * If the service is shutting down, all requests will be rejected with a - * {@link @backstage/errors#ServiceUnavailableError}. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead. - */ -export const createLifecycleMiddleware = _createLifecycleMiddleware; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts deleted file mode 100644 index 0088e53f98..0000000000 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts +++ /dev/null @@ -1,30 +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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { httpRouterServiceFactory as _httpRouterServiceFactory } from '../../../../../backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory'; - -/** - * HTTP route registration for plugins. - * - * See {@link @backstage/code-plugin-api#HttpRouterService} - * and {@link https://backstage.io/docs/backend-system/core-services/http-router | the service docs} - * for more information. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead. - */ -export const httpRouterServiceFactory = _httpRouterServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts deleted file mode 100644 index bc12480ae8..0000000000 --- a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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. - */ - -export { httpRouterServiceFactory } from './httpRouterServiceFactory'; -export { createLifecycleMiddleware } from './createLifecycleMiddleware'; -export type { LifecycleMiddlewareOptions } from './createLifecycleMiddleware'; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index 3a2fe819ed..5f9bb9479a 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -14,15 +14,5 @@ * limitations under the License. */ -export * from './cache'; -export * from './config'; -export * from './database'; -export * from './discovery'; export * from './identity'; -export * from './lifecycle'; -export * from './permissions'; -export * from './rootLifecycle'; export * from './tokenManager'; -export * from './urlReader'; - -export * from './deprecated'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/index.ts b/packages/backend-app-api/src/services/implementations/lifecycle/index.ts deleted file mode 100644 index 8dac4c26b4..0000000000 --- a/packages/backend-app-api/src/services/implementations/lifecycle/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { lifecycleServiceFactory } from './lifecycleServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts deleted file mode 100644 index b3b0135a7c..0000000000 --- a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts +++ /dev/null @@ -1,109 +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 { - LifecycleService, - LifecycleServiceShutdownHook, - LifecycleServiceShutdownOptions, - LifecycleServiceStartupHook, - LifecycleServiceStartupOptions, - LoggerService, - PluginMetadataService, - RootLifecycleService, - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; - -/** - * @internal - * @deprecated - */ -export class BackendPluginLifecycleImpl implements LifecycleService { - constructor( - private readonly logger: LoggerService, - private readonly rootLifecycle: RootLifecycleService, - private readonly pluginMetadata: PluginMetadataService, - ) {} - - #hasStarted = false; - #startupTasks: Array<{ - hook: LifecycleServiceStartupHook; - options?: LifecycleServiceStartupOptions; - }> = []; - - addStartupHook( - hook: LifecycleServiceStartupHook, - options?: LifecycleServiceStartupOptions, - ): void { - if (this.#hasStarted) { - throw new Error('Attempted to add startup hook after startup'); - } - this.#startupTasks.push({ hook, options }); - } - - async startup(): Promise { - if (this.#hasStarted) { - return; - } - this.#hasStarted = true; - - this.logger.debug( - `Running ${this.#startupTasks.length} plugin startup tasks...`, - ); - await Promise.all( - this.#startupTasks.map(async ({ hook, options }) => { - const logger = options?.logger ?? this.logger; - try { - await hook(); - logger.debug(`Plugin startup hook succeeded`); - } catch (error) { - logger.error(`Plugin startup hook failed, ${error}`); - } - }), - ); - } - - addShutdownHook( - hook: LifecycleServiceShutdownHook, - options?: LifecycleServiceShutdownOptions, - ): void { - const plugin = this.pluginMetadata.getId(); - this.rootLifecycle.addShutdownHook(hook, { - logger: options?.logger?.child({ plugin }) ?? this.logger, - }); - } -} - -/** - * Allows plugins to register shutdown hooks that are run when the process is about to exit. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/lifecycle` instead. - */ -export const lifecycleServiceFactory = createServiceFactory({ - service: coreServices.lifecycle, - deps: { - logger: coreServices.logger, - rootLifecycle: coreServices.rootLifecycle, - pluginMetadata: coreServices.pluginMetadata, - }, - async factory({ rootLifecycle, logger, pluginMetadata }) { - return new BackendPluginLifecycleImpl( - logger, - rootLifecycle, - pluginMetadata, - ); - }, -}); diff --git a/packages/backend-app-api/src/services/implementations/logger/index.ts b/packages/backend-app-api/src/services/implementations/logger/index.ts deleted file mode 100644 index ca52bf5193..0000000000 --- a/packages/backend-app-api/src/services/implementations/logger/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { loggerServiceFactory } from './loggerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts deleted file mode 100644 index 7e7b4b98b0..0000000000 --- a/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts +++ /dev/null @@ -1,30 +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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { loggerServiceFactory as _loggerServiceFactory } from '../../../../../backend-defaults/src/entrypoints/logger/loggerServiceFactory'; - -/** - * Plugin-level logging. - * - * See {@link @backstage/code-plugin-api#LoggerService} - * and {@link https://backstage.io/docs/backend-system/core-services/logger | the service docs} - * for more information. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/logger` instead. - */ -export const loggerServiceFactory = _loggerServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/permissions/index.ts b/packages/backend-app-api/src/services/implementations/permissions/index.ts deleted file mode 100644 index 781dda31a0..0000000000 --- a/packages/backend-app-api/src/services/implementations/permissions/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { permissionsServiceFactory } from './permissionsServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts deleted file mode 100644 index c8fa0e7bbf..0000000000 --- a/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts +++ /dev/null @@ -1,42 +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 { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { ServerPermissionClient } from '@backstage/plugin-permission-node'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/permissions` instead. - */ -export const permissionsServiceFactory = createServiceFactory({ - service: coreServices.permissions, - deps: { - auth: coreServices.auth, - config: coreServices.rootConfig, - discovery: coreServices.discovery, - tokenManager: coreServices.tokenManager, - }, - async factory({ auth, config, discovery, tokenManager }) { - return ServerPermissionClient.fromConfig(config, { - auth, - discovery, - tokenManager, - }); - }, -}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.test.ts deleted file mode 100644 index fc1de9b205..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.test.ts +++ /dev/null @@ -1,124 +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 express from 'express'; -import request from 'supertest'; -import { DefaultRootHttpRouter } from './DefaultRootHttpRouter'; - -describe('DefaultRootHttpRouter', () => { - it.each([ - [['/b'], '/a'], - [['/a'], '/aa/b'], - [['/aa'], '/a/b'], - [['/a/b'], '/aa'], - [['/b/a'], '/a'], - [['/a'], '/aa'], - ])(`with existing paths %s, adds %s without conflict`, (existing, added) => { - const router = DefaultRootHttpRouter.create(); - for (const path of existing) { - router.use(path, () => {}); - } - expect(() => router.use(added, () => {})).not.toThrow(); - }); - - it.each([ - [['/a'], '/a', '/a'], - [['/a'], '/a/b', '/a'], - [['/a/b'], '/a', '/a/b'], - ])( - `find conflict when existing paths %s, adds %s`, - (existing, added, conflict) => { - const router = DefaultRootHttpRouter.create(); - for (const path of existing) { - router.use(path, () => {}); - } - expect(() => router.use(added, () => {})).toThrow( - `Path ${added} conflicts with the existing path ${conflict}`, - ); - }, - ); - - it('should not be possible to supply an empty indexPath', () => { - expect(() => DefaultRootHttpRouter.create({ indexPath: '' })).toThrow( - 'indexPath option may not be an empty string', - ); - }); - - it('will always prioritize non-index paths', async () => { - const router = DefaultRootHttpRouter.create({ indexPath: '/x' }); - const app = express(); - app.use(router.handler()); - - const routerX = express.Router(); - routerX.get('/a', (_req, res) => res.status(201).end()); - - const routerY = express.Router(); - routerY.get('/a', (_req, res) => res.status(202).end()); - - await request(app).get('/').expect(404); - await request(app).get('/a').expect(404); - await request(app).get('/x/a').expect(404); - await request(app).get('/y/a').expect(404); - - router.use('/x', routerX); - - await request(app).get('/').expect(404); - await request(app).get('/a').expect(201); - await request(app).get('/x/a').expect(201); - await request(app).get('/y/a').expect(404); - - router.use('/y', routerY); - - await request(app).get('/').expect(404); - await request(app).get('/a').expect(201); - await request(app).get('/x/a').expect(201); - await request(app).get('/y/a').expect(202); - - expect('test').toBe('test'); - }); - - it('should treat unknown /api/ routes as 404', async () => { - const router = DefaultRootHttpRouter.create(); - const app = express(); - app.use(router.handler()); - - router.use('/api/app', (_req, res) => res.status(201).end()); - router.use('/api/catalog', (_req, res) => res.status(202).end()); - - await request(app).get('/').expect(201); - await request(app).get('/api/catalog').expect(202); - await request(app).get('/unknown').expect(201); - await request(app).get('/api/unknown').expect(404); - - expect('test').toBe('test'); - }); - - it('should treat unknown /api/ routes as 404 without an index path', async () => { - const router = DefaultRootHttpRouter.create({ indexPath: false }); - const app = express(); - app.use(router.handler()); - - router.use('/api/app', (_req, res) => res.status(201).end()); - router.use('/api/catalog', (_req, res) => res.status(202).end()); - - await request(app).get('/').expect(404); - await request(app).get('/api/catalog').expect(202); - await request(app).get('/unknown').expect(404); - await request(app).get('/api/unknown').expect(404); - - expect('test').toBe('test'); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts deleted file mode 100644 index 617d5055db..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { RootHttpRouterService } from '@backstage/backend-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - DefaultRootHttpRouter as _DefaultRootHttpRouter, - DefaultRootHttpRouterOptions as _DefaultRootHttpRouterOptions, -} from '../../../../../backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter'; -import { Handler } from 'express'; - -/** - * Options for the {@link DefaultRootHttpRouter} class. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type DefaultRootHttpRouterOptions = _DefaultRootHttpRouterOptions; - -/** - * The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for - * {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export class DefaultRootHttpRouter implements RootHttpRouterService { - static create(options?: DefaultRootHttpRouterOptions) { - return new DefaultRootHttpRouter(_DefaultRootHttpRouter.create(options)); - } - - private constructor(private readonly impl: RootHttpRouterService) {} - - use(path: string, handler: Handler) { - this.impl.use(path, handler); - } - - handler(): Handler { - return (this.impl as any).handler(); - } -} diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts deleted file mode 100644 index 8437399fec..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -export { - rootHttpRouterServiceFactory, - type RootHttpRouterFactoryOptions, - type RootHttpRouterConfigureContext, -} from './rootHttpRouterServiceFactory'; -export { - DefaultRootHttpRouter, - type DefaultRootHttpRouterOptions, -} from './DefaultRootHttpRouter'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts deleted file mode 100644 index bef1913645..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ /dev/null @@ -1,46 +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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - rootHttpRouterServiceFactory as _rootHttpRouterServiceFactory, - RootHttpRouterFactoryOptions as _RootHttpRouterFactoryOptions, - RootHttpRouterConfigureContext as _RootHttpRouterConfigureContext, -} from '../../../../../backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type RootHttpRouterConfigureContext = _RootHttpRouterConfigureContext; - -/** - * HTTP route registration for root services. - * - * See {@link @backstage/code-plugin-api#RootHttpRouterService} - * and {@link https://backstage.io/docs/backend-system/core-services/root-http-router | the service docs} - * for more information. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export type RootHttpRouterFactoryOptions = _RootHttpRouterFactoryOptions; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead. - */ -export const rootHttpRouterServiceFactory = _rootHttpRouterServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts deleted file mode 100644 index 86589cd23e..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { rootLifecycleServiceFactory } from './rootLifecycleServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts deleted file mode 100644 index 5992e38d50..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts +++ /dev/null @@ -1,60 +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 { BackendLifecycleImpl } from './rootLifecycleServiceFactory'; -import { mockServices } from '@backstage/backend-test-utils'; - -describe('lifecycleService', () => { - it('should execute registered shutdown hook', async () => { - const service = new BackendLifecycleImpl(mockServices.logger.mock()); - const hook = jest.fn(); - service.addShutdownHook(() => hook()); - // should not execute the hook more than once. - await service.shutdown(); - await service.shutdown(); - await service.shutdown(); - expect(hook).toHaveBeenCalledTimes(1); - }); - - it('should not throw errors', async () => { - const service = new BackendLifecycleImpl(mockServices.logger.mock()); - service.addShutdownHook(() => { - throw new Error('oh no'); - }); - await expect(service.shutdown()).resolves.toBeUndefined(); - }); - - it('should not throw async errors', async () => { - const service = new BackendLifecycleImpl(mockServices.logger.mock()); - service.addShutdownHook(async () => { - throw new Error('oh no'); - }); - await expect(service.shutdown()).resolves.toBeUndefined(); - }); - - it('should reject hooks after trigger', async () => { - const service = new BackendLifecycleImpl(mockServices.logger.mock()); - await service.startup(); - expect(() => { - service.addStartupHook(() => {}); - }).toThrow('Attempted to add startup hook after startup'); - - await service.shutdown(); - expect(() => { - service.addShutdownHook(() => {}); - }).toThrow('Attempted to add shutdown hook after shutdown'); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts deleted file mode 100644 index bf5dc09b80..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts +++ /dev/null @@ -1,124 +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 { - createServiceFactory, - coreServices, - LifecycleServiceStartupHook, - LifecycleServiceStartupOptions, - LifecycleServiceShutdownHook, - LifecycleServiceShutdownOptions, - RootLifecycleService, - LoggerService, -} from '@backstage/backend-plugin-api'; - -/** - * @internal - * @deprecated - */ -export class BackendLifecycleImpl implements RootLifecycleService { - constructor(private readonly logger: LoggerService) {} - - #hasStarted = false; - #startupTasks: Array<{ - hook: LifecycleServiceStartupHook; - options?: LifecycleServiceStartupOptions; - }> = []; - - addStartupHook( - hook: LifecycleServiceStartupHook, - options?: LifecycleServiceStartupOptions, - ): void { - if (this.#hasStarted) { - throw new Error('Attempted to add startup hook after startup'); - } - this.#startupTasks.push({ hook, options }); - } - - async startup(): Promise { - if (this.#hasStarted) { - return; - } - this.#hasStarted = true; - - this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`); - await Promise.all( - this.#startupTasks.map(async ({ hook, options }) => { - const logger = options?.logger ?? this.logger; - try { - await hook(); - logger.debug(`Startup hook succeeded`); - } catch (error) { - logger.error(`Startup hook failed, ${error}`); - } - }), - ); - } - - #hasShutdown = false; - #shutdownTasks: Array<{ - hook: LifecycleServiceShutdownHook; - options?: LifecycleServiceShutdownOptions; - }> = []; - - addShutdownHook( - hook: LifecycleServiceShutdownHook, - options?: LifecycleServiceShutdownOptions, - ): void { - if (this.#hasShutdown) { - throw new Error('Attempted to add shutdown hook after shutdown'); - } - this.#shutdownTasks.push({ hook, options }); - } - - async shutdown(): Promise { - if (this.#hasShutdown) { - return; - } - this.#hasShutdown = true; - - this.logger.debug( - `Running ${this.#shutdownTasks.length} shutdown tasks...`, - ); - await Promise.all( - this.#shutdownTasks.map(async ({ hook, options }) => { - const logger = options?.logger ?? this.logger; - try { - await hook(); - logger.debug(`Shutdown hook succeeded`); - } catch (error) { - logger.error(`Shutdown hook failed, ${error}`); - } - }), - ); - } -} - -/** - * Allows plugins to register shutdown hooks that are run when the process is about to exit. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootLifecycle` instead. - */ -export const rootLifecycleServiceFactory = createServiceFactory({ - service: coreServices.rootLifecycle, - deps: { - logger: coreServices.rootLogger, - }, - async factory({ logger }) { - return new BackendLifecycleImpl(logger); - }, -}); diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/index.ts b/packages/backend-app-api/src/services/implementations/rootLogger/index.ts deleted file mode 100644 index 366cd53163..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootLogger/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts deleted file mode 100644 index af1931c438..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts +++ /dev/null @@ -1,30 +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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { rootLoggerServiceFactory as _rootLoggerServiceFactory } from '../../../../../backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory'; - -/** - * Root-level logging. - * - * See {@link @backstage/code-plugin-api#RootLoggerService} - * and {@link https://backstage.io/docs/backend-system/core-services/root-logger | the service docs} - * for more information. - * - * @public - * @deprecated Please import from `@backstage/backend-defaults/rootLogger` instead. - */ -export const rootLoggerServiceFactory = _rootLoggerServiceFactory; diff --git a/packages/backend-app-api/src/services/implementations/scheduler/index.ts b/packages/backend-app-api/src/services/implementations/scheduler/index.ts deleted file mode 100644 index 8b67006a4d..0000000000 --- a/packages/backend-app-api/src/services/implementations/scheduler/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { schedulerServiceFactory } from './schedulerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts deleted file mode 100644 index b16509cf96..0000000000 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { coreServices } from '@backstage/backend-plugin-api'; -import { ServiceFactoryTester } from '@backstage/backend-test-utils'; -import { schedulerServiceFactory } from './schedulerServiceFactory'; - -describe('schedulerFactory', () => { - it('creates sidecar database features', async () => { - const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - - const scheduler = await tester.getSubject(); - await scheduler.scheduleTask({ - id: 'task1', - timeout: { seconds: 1 }, - frequency: { seconds: 1 }, - fn: async () => {}, - }); - - const database = await tester.getService(coreServices.database); - - const client = await database.getClient(); - await expect( - client.from('backstage_backend_tasks__tasks').count(), - ).resolves.toEqual([{ 'count(*)': 1 }]); - await expect( - client.from('backstage_backend_tasks__knex_migrations').count(), - ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); - await expect( - client.from('backstage_backend_tasks__knex_migrations_lock').count(), - ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts deleted file mode 100644 index b4370761d9..0000000000 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts +++ /dev/null @@ -1,41 +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 { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { TaskScheduler } from '@backstage/backend-tasks'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/scheduler` instead. - */ -export const schedulerServiceFactory = createServiceFactory({ - service: coreServices.scheduler, - deps: { - plugin: coreServices.pluginMetadata, - databaseManager: coreServices.database, - logger: coreServices.logger, - }, - async factory({ plugin, databaseManager, logger }) { - return TaskScheduler.forPlugin({ - pluginId: plugin.getId(), - databaseManager, - logger, - }); - }, -}); diff --git a/packages/backend-app-api/src/services/implementations/urlReader/index.ts b/packages/backend-app-api/src/services/implementations/urlReader/index.ts deleted file mode 100644 index 6a4b9f65be..0000000000 --- a/packages/backend-app-api/src/services/implementations/urlReader/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export { urlReaderServiceFactory } from './urlReaderServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts deleted file mode 100644 index 44caf25ad6..0000000000 --- a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts +++ /dev/null @@ -1,39 +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 { UrlReaders } from '@backstage/backend-common'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/urlReader` instead. - */ -export const urlReaderServiceFactory = createServiceFactory({ - service: coreServices.urlReader, - deps: { - config: coreServices.rootConfig, - logger: coreServices.logger, - }, - async factory({ config, logger }) { - return UrlReaders.default({ - config, - logger, - }); - }, -}); 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/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts index 0dd3ba4423..9df7549bcb 100644 --- a/packages/backend-app-api/src/wiring/BackstageBackend.ts +++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts @@ -25,12 +25,7 @@ export class BackstageBackend implements Backend { this.#initializer = new BackendInitializer(defaultServiceFactories); } - add( - feature: - | BackendFeature - | (() => BackendFeature) - | Promise<{ default: BackendFeature | (() => BackendFeature) }>, - ): void { + add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void { if (isPromise(feature)) { this.#initializer.add(feature.then(f => unwrapFeature(f.default))); } else { @@ -57,15 +52,8 @@ function isPromise(value: unknown | Promise): value is Promise { } function unwrapFeature( - feature: - | BackendFeature - | (() => BackendFeature) - | { default: BackendFeature | (() => BackendFeature) }, + feature: BackendFeature | { default: BackendFeature }, ): BackendFeature { - if (typeof feature === 'function') { - return feature(); - } - if ('$$type' in feature) { return feature; } @@ -75,10 +63,7 @@ function unwrapFeature( // when importing using a dynamic import. // TODO: This is a broader issue than just this piece of code, and should move away from CommonJS. if ('default' in feature) { - const defaultFeature = feature.default; - return typeof defaultFeature === 'function' - ? defaultFeature() - : defaultFeature; + return feature.default; } return feature; 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-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 075eb58b3f..bd761ff3c8 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -26,18 +26,6 @@ import { */ export interface Backend { add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void; - /** - * @deprecated The ability to add features defined as a callback is being - * removed. Please update the installed feature to no longer be defined as a - * callback, typically this means updating it to use the latest version of - * `@backstage/backend-plugin-api`. If the feature is from a third-party - * package, please reach out to the package maintainer to update it. - */ - add( - feature: - | (() => BackendFeature) - | Promise<{ default: () => BackendFeature }>, - ): void; start(): Promise; stop(): Promise; } diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 2bd24bc8d5..9907005ba9 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/backend-common +## 0.23.4-next.3 + +### Patch Changes + +- ddde5fe: Internal type refactor. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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/api-report.md b/packages/backend-common/api-report.md index 23ed13b62c..61efc0f3a1 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -9,30 +9,18 @@ import { AppConfig } from '@backstage/config'; import { AuthCallback } from 'isomorphic-git'; import { AuthService } from '@backstage/backend-plugin-api'; -import { AwsCredentialsManager } from '@backstage/integration-aws-node'; -import { AwsS3Integration } from '@backstage/integration'; -import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; -import { AzureIntegration } from '@backstage/integration'; import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; -import { BitbucketCloudIntegration } from '@backstage/integration'; -import { BitbucketIntegration } from '@backstage/integration'; -import { BitbucketServerIntegration } from '@backstage/integration'; import { CacheService } from '@backstage/backend-plugin-api'; import { CacheServiceOptions } from '@backstage/backend-plugin-api'; import { CacheServiceSetOptions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; +import { ConfigSchema } from '@backstage/config-loader'; import cors from 'cors'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import Docker from 'dockerode'; import { ErrorRequestHandler } from 'express'; import express from 'express'; -import { GerritIntegration } from '@backstage/integration'; -import { GiteaIntegration } from '@backstage/integration'; -import { GithubCredentialsProvider } from '@backstage/integration'; -import { GithubIntegration } from '@backstage/integration'; -import { GitLabIntegration } from '@backstage/integration'; -import { HarnessIntegration } from '@backstage/integration'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; import { isChildPath as isChildPath_2 } from '@backstage/backend-plugin-api'; @@ -46,34 +34,18 @@ import { MergeResult } from 'isomorphic-git'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; -import { Readable } from 'stream'; import { ReadCommitResult } from 'isomorphic-git'; -import { ReadTreeOptions as ReadTreeOptions_2 } from '@backstage/backend-plugin-api'; -import { ReadTreeResponse as ReadTreeResponse_2 } from '@backstage/backend-plugin-api'; -import { ReadTreeResponseDirOptions as ReadTreeResponseDirOptions_2 } from '@backstage/backend-plugin-api'; -import { ReadTreeResponseFile as ReadTreeResponseFile_2 } from '@backstage/backend-plugin-api'; -import { ReadUrlOptions as ReadUrlOptions_2 } from '@backstage/backend-plugin-api'; -import { ReadUrlResponse as ReadUrlResponse_2 } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { resolvePackagePath as resolvePackagePath_2 } from '@backstage/backend-plugin-api'; import { resolveSafeChildPath as resolveSafeChildPath_2 } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; import { SchedulerService } from '@backstage/backend-plugin-api'; -import { SearchOptions as SearchOptions_2 } from '@backstage/backend-plugin-api'; -import { SearchResponse as SearchResponse_2 } from '@backstage/backend-plugin-api'; -import { SearchResponseFile as SearchResponseFile_2 } from '@backstage/backend-plugin-api'; import { Server } from 'http'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { TransportStreamOptions } from 'winston-transport'; import { UrlReaderService } from '@backstage/backend-plugin-api'; -import { UrlReaderServiceReadTreeOptions } from '@backstage/backend-plugin-api'; -import { UrlReaderServiceReadTreeResponse } from '@backstage/backend-plugin-api'; -import { UrlReaderServiceReadUrlOptions } from '@backstage/backend-plugin-api'; -import { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api'; -import { UrlReaderServiceSearchOptions } from '@backstage/backend-plugin-api'; -import { UrlReaderServiceSearchResponse } from '@backstage/backend-plugin-api'; import { UserInfoService } from '@backstage/backend-plugin-api'; import { V1PodTemplateSpec } from '@kubernetes/client-node'; import * as winston from 'winston'; @@ -85,31 +57,6 @@ export type AuthCallbackOptions = { logger?: LoggerService; }; -// Warning: (ae-forgotten-export) The symbol "AwsS3UrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class AwsS3UrlReader extends AwsS3UrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "AzureUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class AzureUrlReader extends AzureUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "BitbucketCloudUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class BitbucketCloudUrlReader extends BitbucketCloudUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "BitbucketServerUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class BitbucketServerUrlReader extends BitbucketServerUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "BitbucketUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class BitbucketUrlReader extends BitbucketUrlReader_2 {} - // @public @deprecated (undocumented) export type CacheClient = CacheService; @@ -142,6 +89,11 @@ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise; } +// Warning: (ae-forgotten-export) The symbol "createConfigSecretEnumerator_2" needs to be exported by the entry point index.d.ts +// +// @public @deprecated (undocumented) +export const createConfigSecretEnumerator: typeof createConfigSecretEnumerator_2; + // @public @deprecated export function createLegacyAuthAdapters< TOptions extends { @@ -239,21 +191,6 @@ export type ErrorHandlerOptions = { logClientErrors?: boolean; }; -// Warning: (ae-forgotten-export) The symbol "FetchUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class FetchUrlReader extends FetchUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "FromReadableArrayOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type FromReadableArrayOptions = FromReadableArrayOptions_2; - -// Warning: (ae-forgotten-export) The symbol "GerritUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class GerritUrlReader extends GerritUrlReader_2 {} - // @public @deprecated export function getRootLogger(): winston.Logger; @@ -336,26 +273,6 @@ export class Git { resolveRef(options: { dir: string; ref: string }): Promise; } -// Warning: (ae-forgotten-export) The symbol "GiteaUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class GiteaUrlReader extends GiteaUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "GithubUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class GithubUrlReader extends GithubUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "GitlabUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class GitlabUrlReader extends GitlabUrlReader_2 {} - -// Warning: (ae-forgotten-export) The symbol "HarnessUrlReader_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class HarnessUrlReader extends HarnessUrlReader_2 {} - // @public @deprecated class HostDiscovery implements DiscoveryService { static fromConfig( @@ -500,50 +417,6 @@ export interface PullOptions { }; } -// Warning: (ae-forgotten-export) The symbol "ReaderFactory_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type ReaderFactory = ReaderFactory_2; - -// @public @deprecated (undocumented) -export type ReadTreeOptions = ReadTreeOptions_2; - -// @public @deprecated (undocumented) -export type ReadTreeResponse = ReadTreeResponse_2; - -// @public @deprecated (undocumented) -export type ReadTreeResponseDirOptions = ReadTreeResponseDirOptions_2; - -// Warning: (ae-forgotten-export) The symbol "ReadTreeResponseFactory_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type ReadTreeResponseFactory = ReadTreeResponseFactory_2; - -// Warning: (ae-forgotten-export) The symbol "ReadTreeResponseFactoryOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type ReadTreeResponseFactoryOptions = ReadTreeResponseFactoryOptions_2; - -// @public @deprecated (undocumented) -export type ReadTreeResponseFile = ReadTreeResponseFile_2; - -// @public @deprecated (undocumented) -export type ReadUrlOptions = ReadUrlOptions_2; - -// @public @deprecated (undocumented) -export type ReadUrlResponse = ReadUrlResponse_2; - -// Warning: (ae-forgotten-export) The symbol "ReadUrlResponseFactory_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class ReadUrlResponseFactory extends ReadUrlResponseFactory_2 {} - -// Warning: (ae-forgotten-export) The symbol "ReadUrlResponseFactoryFromStreamOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type ReadUrlResponseFactoryFromStreamOptions = - ReadUrlResponseFactoryFromStreamOptions_2; - // @public @deprecated export function redactWinstonLogLine( info: winston.Logform.TransformableInfo, @@ -577,15 +450,6 @@ export type RunContainerOptions = { pullOptions?: PullOptions; }; -// @public @deprecated (undocumented) -export type SearchOptions = SearchOptions_2; - -// @public @deprecated (undocumented) -export type SearchResponse = SearchResponse_2; - -// @public @deprecated (undocumented) -export type SearchResponseFile = SearchResponseFile_2; - // @public @deprecated export class ServerTokenManager implements TokenManager { // (undocumented) @@ -661,24 +525,6 @@ export interface StatusCheckHandlerOptions { // @public @deprecated (undocumented) export type TokenManager = TokenManagerService; -// @public @deprecated (undocumented) -export type UrlReader = UrlReaderService; - -// Warning: (ae-forgotten-export) The symbol "UrlReaderPredicateTuple_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type UrlReaderPredicateTuple = UrlReaderPredicateTuple_2; - -// Warning: (ae-forgotten-export) The symbol "UrlReaders_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export class UrlReaders extends UrlReaders_2 {} - -// Warning: (ae-forgotten-export) The symbol "UrlReadersOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type UrlReadersOptions = UrlReadersOptions_2; - // @public @deprecated export function useHotCleanup( _module: NodeModule, diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 17913d87d4..c088e32e4d 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.3", "description": "Common functionality library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-common/src/compat/legacy/legacy.test.ts b/packages/backend-common/src/compat/legacy/legacy.test.ts index e5d395a8f9..08a645f660 100644 --- a/packages/backend-common/src/compat/legacy/legacy.test.ts +++ b/packages/backend-common/src/compat/legacy/legacy.test.ts @@ -24,7 +24,7 @@ import { Router } from 'express'; import { createLegacyAuthAdapters } from '..'; import { legacyPlugin } from './legacy'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { authServiceFactory } from '../../../../backend-app-api/src/services/implementations/auth'; +import { authServiceFactory } from '../../../../backend-defaults/src/entrypoints/auth'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { tokenManagerServiceFactory } from '../../../../backend-app-api/src/services/implementations/tokenManager'; 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-common/src/deprecated/config.ts b/packages/backend-common/src/deprecated/config.ts deleted file mode 100644 index 2d06ca2072..0000000000 --- a/packages/backend-common/src/deprecated/config.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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. - */ - -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - createConfigSecretEnumerator, - loadBackendConfig as newLoadBackendConfig, -} from '../../../backend-app-api/src/config'; -import { LoggerService } from '@backstage/backend-plugin-api'; -import { AppConfig, Config } from '@backstage/config'; -import { LoadConfigOptionsRemote } from '@backstage/config-loader'; -import { setRootLoggerRedactionList } from './logging/createRootLogger'; - -/** - * Load configuration for a Backend. - * - * This function should only be called once, during the initialization of the backend. - * - * @public - * @deprecated Use {@link @backstage/backend-app-api#loadBackendConfig} instead. - */ -export async function loadBackendConfig(options: { - logger: LoggerService; - // process.argv or any other overrides - remote?: LoadConfigOptionsRemote; - additionalConfigs?: AppConfig[]; - argv: string[]; - watch?: boolean; -}): Promise { - const secretEnumerator = await createConfigSecretEnumerator({ - logger: options.logger, - }); - const { config } = await newLoadBackendConfig(options); - - setRootLoggerRedactionList(secretEnumerator(config)); - config.subscribe?.(() => - setRootLoggerRedactionList(secretEnumerator(config)), - ); - - return config; -} diff --git a/packages/backend-app-api/src/config/ObservableConfigProxy.test.ts b/packages/backend-common/src/deprecated/config/ObservableConfigProxy.test.ts similarity index 100% rename from packages/backend-app-api/src/config/ObservableConfigProxy.test.ts rename to packages/backend-common/src/deprecated/config/ObservableConfigProxy.test.ts diff --git a/packages/backend-app-api/src/config/ObservableConfigProxy.ts b/packages/backend-common/src/deprecated/config/ObservableConfigProxy.ts similarity index 100% rename from packages/backend-app-api/src/config/ObservableConfigProxy.ts rename to packages/backend-common/src/deprecated/config/ObservableConfigProxy.ts diff --git a/packages/backend-app-api/src/config/config.test.ts b/packages/backend-common/src/deprecated/config/config.test.ts similarity index 100% rename from packages/backend-app-api/src/config/config.test.ts rename to packages/backend-common/src/deprecated/config/config.test.ts index 8828c557e0..865b1b4d60 100644 --- a/packages/backend-app-api/src/config/config.test.ts +++ b/packages/backend-common/src/deprecated/config/config.test.ts @@ -15,8 +15,8 @@ */ import { loadConfigSchema } from '@backstage/config-loader'; -import { createConfigSecretEnumerator } from './config'; import { mockServices } from '@backstage/backend-test-utils'; +import { createConfigSecretEnumerator } from './config'; describe('createConfigSecretEnumerator', () => { it('should enumerate secrets', async () => { diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-common/src/deprecated/config/config.ts similarity index 76% rename from packages/backend-app-api/src/config/config.ts rename to packages/backend-common/src/deprecated/config/config.ts index 957997849e..d50476a9f2 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-common/src/deprecated/config/config.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import { LoggerService } from '@backstage/backend-plugin-api'; +import { AppConfig, Config } from '@backstage/config'; +import { setRootLoggerRedactionList } from '../logging/createRootLogger'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { createConfigSecretEnumerator as _createConfigSecretEnumerator } from '../../../backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator'; +import { createConfigSecretEnumerator as _createConfigSecretEnumerator } from '../../../../backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator'; import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; @@ -26,13 +29,12 @@ import { LoadConfigOptionsRemote, } from '@backstage/config-loader'; import { ConfigReader } from '@backstage/config'; -import type { Config, AppConfig } from '@backstage/config'; import { ObservableConfigProxy } from './ObservableConfigProxy'; -import { isValidUrl } from '../lib/urls'; +import { isValidUrl } from './urls'; /** * @public - * @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead. + * @deprecated Please migrate to the new backend system and use `coreServices.rootConfig` instead, or the {@link @backstage/config-loader#ConfigSources} facilities if required. */ export const createConfigSecretEnumerator = _createConfigSecretEnumerator; @@ -45,6 +47,27 @@ export const createConfigSecretEnumerator = _createConfigSecretEnumerator; * @deprecated Please migrate to the new backend system and use `coreServices.rootConfig` instead, or the {@link @backstage/config-loader#ConfigSources} facilities if required. */ export async function loadBackendConfig(options: { + logger: LoggerService; + // process.argv or any other overrides + remote?: LoadConfigOptionsRemote; + additionalConfigs?: AppConfig[]; + argv: string[]; + watch?: boolean; +}): Promise { + const secretEnumerator = await createConfigSecretEnumerator({ + logger: options.logger, + }); + const { config } = await newLoadBackendConfig(options); + + setRootLoggerRedactionList(secretEnumerator(config)); + config.subscribe?.(() => + setRootLoggerRedactionList(secretEnumerator(config)), + ); + + return config; +} + +async function newLoadBackendConfig(options: { remote?: LoadConfigOptionsRemote; argv: string[]; additionalConfigs?: AppConfig[]; diff --git a/packages/backend-app-api/src/config/index.ts b/packages/backend-common/src/deprecated/config/index.ts similarity index 94% rename from packages/backend-app-api/src/config/index.ts rename to packages/backend-common/src/deprecated/config/index.ts index 0ed8d2bc93..c87193876c 100644 --- a/packages/backend-app-api/src/config/index.ts +++ b/packages/backend-common/src/deprecated/config/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. diff --git a/packages/backend-app-api/src/lib/urls.test.ts b/packages/backend-common/src/deprecated/config/urls.test.ts similarity index 100% rename from packages/backend-app-api/src/lib/urls.test.ts rename to packages/backend-common/src/deprecated/config/urls.test.ts diff --git a/packages/backend-app-api/src/lib/urls.ts b/packages/backend-common/src/deprecated/config/urls.ts similarity index 100% rename from packages/backend-app-api/src/lib/urls.ts rename to packages/backend-common/src/deprecated/config/urls.ts diff --git a/packages/backend-common/src/deprecated/index.ts b/packages/backend-common/src/deprecated/index.ts index e3e4101d25..5dfcd322ed 100644 --- a/packages/backend-common/src/deprecated/index.ts +++ b/packages/backend-common/src/deprecated/index.ts @@ -36,45 +36,6 @@ import { type LegacyRootDatabaseService as _LegacyRootDatabaseService, } from '../../../backend-defaults/src/entrypoints/database/DatabaseManager'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { AzureUrlReader as _AzureUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { BitbucketCloudUrlReader as _BitbucketCloudUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { BitbucketUrlReader as _BitbucketUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { BitbucketServerUrlReader as _BitbucketServerUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { GerritUrlReader as _GerritUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { GithubUrlReader as _GithubUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { GitlabUrlReader as _GitlabUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { GiteaUrlReader as _GiteaUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { HarnessUrlReader as _HarnessUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { AwsS3UrlReader as _AwsS3UrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { FetchUrlReader as _FetchUrlReader } from '../../../backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { UrlReaders as _UrlReaders } from '../../../backend-defaults/src/entrypoints/urlReader/lib/UrlReaders'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { ReadUrlResponseFactory as _ReadUrlResponseFactory } from '../../../backend-defaults/src/entrypoints/urlReader/lib/ReadUrlResponseFactory'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import type { UrlReadersOptions as _UrlReadersOptions } from '../../../backend-defaults/src/entrypoints/urlReader/lib/UrlReaders'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import type { FromReadableArrayOptions as _FromReadableArrayOptions } from '../../../backend-defaults/src/entrypoints/urlReader/lib/types'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import type { - ReaderFactory as _ReaderFactory, - ReadTreeResponseFactory as _ReadTreeResponseFactory, - ReadTreeResponseFactoryOptions as _ReadTreeResponseFactoryOptions, - ReadUrlResponseFactoryFromStreamOptions as _ReadUrlResponseFactoryFromStreamOptions, - UrlReaderPredicateTuple as _UrlReaderPredicateTuple, -} from '../../../backend-defaults/src/entrypoints/urlReader/lib/types'; - import { DiscoveryService, CacheService, @@ -85,16 +46,6 @@ import { resolvePackagePath as _resolvePackagePath, resolveSafeChildPath as _resolveSafeChildPath, isChildPath as _isChildPath, - ReadTreeOptions as _ReadTreeOptions, - ReadTreeResponse as _ReadTreeResponse, - ReadTreeResponseFile as _ReadTreeResponseFile, - ReadTreeResponseDirOptions as _ReadTreeResponseDirOptions, - ReadUrlOptions as _ReadUrlOptions, - ReadUrlResponse as _ReadUrlResponse, - SearchOptions as _SearchOptions, - SearchResponse as _SearchResponse, - SearchResponseFile as _SearchResponseFile, - UrlReaderService as _UrlReaderService, LifecycleService, PluginMetadataService, } from '@backstage/backend-plugin-api'; @@ -290,184 +241,3 @@ export const resolveSafeChildPath = _resolveSafeChildPath; * Please use the `isChildPath` function from the `@backstage/cli-common` package instead. */ export const isChildPath = _isChildPath; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class AzureUrlReader extends _AzureUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class BitbucketCloudUrlReader extends _BitbucketCloudUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class BitbucketUrlReader extends _BitbucketUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class BitbucketServerUrlReader extends _BitbucketServerUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class GerritUrlReader extends _GerritUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class GithubUrlReader extends _GithubUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class GitlabUrlReader extends _GitlabUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class GiteaUrlReader extends _GiteaUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class HarnessUrlReader extends _HarnessUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class AwsS3UrlReader extends _AwsS3UrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class FetchUrlReader extends _FetchUrlReader {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class UrlReaders extends _UrlReaders {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export class ReadUrlResponseFactory extends _ReadUrlResponseFactory {} - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type UrlReadersOptions = _UrlReadersOptions; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type FromReadableArrayOptions = _FromReadableArrayOptions; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type ReaderFactory = _ReaderFactory; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type ReadTreeResponseFactory = _ReadTreeResponseFactory; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type ReadTreeResponseFactoryOptions = _ReadTreeResponseFactoryOptions; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type ReadUrlResponseFactoryFromStreamOptions = - _ReadUrlResponseFactoryFromStreamOptions; - -/** - * @public - * @deprecated Import from `@backstage/backend-defaults/urlReader` instead - */ -export type UrlReaderPredicateTuple = _UrlReaderPredicateTuple; - -/** - * @public - * @deprecated Use `UrlReaderServiceReadTreeOptions` from `@backstage/backend-plugin-api` instead - */ -export type ReadTreeOptions = _ReadTreeOptions; - -/** - * @public - * @deprecated Use `UrlReaderServiceReadTreeResponse` from `@backstage/backend-plugin-api` instead - */ -export type ReadTreeResponse = _ReadTreeResponse; - -/** - * @public - * @deprecated Use `UrlReaderServiceReadTreeResponseFile` from `@backstage/backend-plugin-api` instead - */ -export type ReadTreeResponseFile = _ReadTreeResponseFile; - -/** - * @public - * @deprecated Use `UrlReaderServiceReadTreeResponseDirOptions` from `@backstage/backend-plugin-api` instead - */ -export type ReadTreeResponseDirOptions = _ReadTreeResponseDirOptions; - -/** - * @public - * @deprecated Use `UrlReaderServiceReadUrlOptions` from `@backstage/backend-plugin-api` instead - */ -export type ReadUrlOptions = _ReadUrlOptions; - -/** - * @public - * @deprecated Use `UrlReaderServiceReadUrlResponse` from `@backstage/backend-plugin-api` instead - */ -export type ReadUrlResponse = _ReadUrlResponse; - -/** - * @public - * @deprecated Use `UrlReaderServiceSearchOptions` from `@backstage/backend-plugin-api` instead - */ -export type SearchOptions = _SearchOptions; - -/** - * @public - * @deprecated Use `UrlReaderServiceSearchResponse` from `@backstage/backend-plugin-api` instead - */ -export type SearchResponse = _SearchResponse; - -/** - * @public - * @deprecated Use `UrlReaderServiceSearchResponseFile` from `@backstage/backend-plugin-api` instead - */ -export type SearchResponseFile = _SearchResponseFile; - -/** - * @public - * @deprecated Use `UrlReaderService` from `@backstage/backend-plugin-api` instead - */ -export type UrlReader = _UrlReaderService; diff --git a/packages/backend-common/src/deprecated/logging/createRootLogger.ts b/packages/backend-common/src/deprecated/logging/createRootLogger.ts index b177024601..04b3799381 100644 --- a/packages/backend-common/src/deprecated/logging/createRootLogger.ts +++ b/packages/backend-common/src/deprecated/logging/createRootLogger.ts @@ -15,7 +15,7 @@ */ // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { WinstonLogger } from '../../../../backend-app-api/src/logging/WinstonLogger'; +import { WinstonLogger } from '../../../../backend-defaults/src/entrypoints/rootLogger/WinstonLogger'; import { merge } from 'lodash'; import * as winston from 'winston'; import { format, LoggerOptions } from 'winston'; diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 387c9d197e..eaf4165241 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/backend-defaults +## 0.4.2-next.3 + +### Patch Changes + +- 81f930a: use formatted query to prevent chance of SQL-injection +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-app-api@0.8.1-next.3 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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 196c9b188a..0da8bb9d91 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.3", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" @@ -171,6 +171,7 @@ "path-to-regexp": "^6.2.1", "pg": "^8.11.3", "pg-connection-string": "^2.3.0", + "pg-format": "^1.0.4", "raw-body": "^2.4.1", "selfsigned": "^2.0.0", "stoppable": "^1.1.0", @@ -191,6 +192,7 @@ "@types/http-errors": "^2.0.0", "@types/morgan": "^1.9.0", "@types/node-forge": "^1.3.0", + "@types/pg-format": "^1.0.5", "@types/stoppable": "^1.1.0", "aws-sdk-client-mock": "^4.0.0", "http-errors": "^2.0.0", diff --git a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts index 159da79435..08cfc147f8 100644 --- a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts +++ b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts @@ -22,6 +22,7 @@ import { BackstagePrincipalTypes, BackstageServicePrincipal, BackstageUserPrincipal, + LoggerService, } from '@backstage/backend-plugin-api'; import { AuthenticationError, ForwardedError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; @@ -47,6 +48,7 @@ export class DefaultAuthService implements AuthService { private readonly pluginId: string, private readonly disableDefaultAuthPolicy: boolean, private readonly pluginKeySource: PluginKeySource, + private readonly logger: LoggerService, ) {} async authenticate( @@ -166,6 +168,9 @@ export class DefaultAuthService implements AuthService { }); } // If the target plugin does not support the new auth service, fall back to using old token format + this.logger.warn( + `DEPRECATION WARNING: A call to the '${targetPluginId}' plugin had to fall back to using deprecated auth via the token manager service. Please migrate all plugins to the new auth service, see https://backstage.io/docs/tutorials/auth-service-migration for more information`, + ); return this.tokenManager.getToken().catch(error => { throw new ForwardedError( `Unable to generate legacy token for communication with the '${targetPluginId}' plugin. ` + diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index e275e2be00..b66ba7227f 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -88,6 +88,7 @@ export const authServiceFactory = createServiceFactory({ plugin.getId(), disableDefaultAuthPolicy, keySource, + logger, ); }, }); 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/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts index 444bbc0d18..46b958789d 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts @@ -29,6 +29,7 @@ import { Connector } from '../types'; import defaultNameOverride from './defaultNameOverride'; import defaultSchemaOverride from './defaultSchemaOverride'; import { mergeDatabaseConfig } from './mergeDatabaseConfig'; +import format from 'pg-format'; // Limits the number of concurrent DDL operations to 1 const ddlLimiter = limiterFactory(1); @@ -52,7 +53,8 @@ export function createPgDatabaseClient( database.client.pool.on( 'createSuccess', async (_event: number, pgClient: Client) => { - await pgClient.query(`SET ROLE ${role}`); + const query = format('SET ROLE %I', role); + await pgClient.query(query); }, ); } diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts index 77136fd7e5..f38c10820a 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts @@ -140,8 +140,9 @@ export class MiddlewareFactory { const logger = this.#logger.child({ type: 'incomingRequest', }); - - return morgan('combined', { + const customMorganFormat = + '[:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'; + return morgan(customMorganFormat, { stream: { write(message: string) { logger.info(message.trimEnd()); 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-dev-utils/src/ipcClient.ts b/packages/backend-dev-utils/src/ipcClient.ts index d9ec7821de..6e1d3d181e 100644 --- a/packages/backend-dev-utils/src/ipcClient.ts +++ b/packages/backend-dev-utils/src/ipcClient.ts @@ -35,6 +35,8 @@ type Response = error: Error; }; +type ResponseHandler = (response: Response) => void; + const requestType = '@backstage/cli/channel/request'; const responseType = '@backstage/cli/channel/response'; @@ -48,21 +50,40 @@ const IPC_TIMEOUT_MS = 5000; export class BackstageIpcClient { #messageId = 0; #sendMessage: SendMessage; + #handlers: Map = new Map(); /** * Creates a new client if we're in a child process with IPC and BACKSTAGE_CLI_CHANNEL is set. */ static create(): BackstageIpcClient | undefined { const sendMessage = process.send?.bind(process); - return sendMessage && process.env.BACKSTAGE_CLI_CHANNEL - ? new BackstageIpcClient(sendMessage) - : undefined; + const client = + sendMessage && process.env.BACKSTAGE_CLI_CHANNEL + ? new BackstageIpcClient(sendMessage) + : undefined; + + if (client) { + process.on('message', (message, _sendHandle) => + client.handleMessage(message, _sendHandle), + ); + } + + return client; } constructor(sendMessage: SendMessage) { this.#sendMessage = sendMessage; } + private handleMessage(message: unknown, _sendHandle: unknown) { + const isResponse = (msg: unknown): msg is Response => + (msg as Response)?.type === responseType; + + if (isResponse(message)) { + this.#handlers.get(message.id)?.(message); + } + } + /** * Send a request to the parent process and wait for a response. */ @@ -82,17 +103,10 @@ export class BackstageIpcClient { let timeout: NodeJS.Timeout | undefined = undefined; - const messageHandler = (response: Response) => { - if (response?.type !== responseType) { - return; - } - if (response.id !== id) { - return; - } - + const responseHandler: ResponseHandler = (response: Response) => { clearTimeout(timeout); timeout = undefined; - process.removeListener('message', messageHandler); + this.#handlers.delete(id); if ('error' in response) { const error = new Error(response.error.message); @@ -107,12 +121,11 @@ export class BackstageIpcClient { timeout = setTimeout(() => { reject(new Error(`IPC request '${method}' with ID ${id} timed out`)); - process.removeListener('message', messageHandler); + this.#handlers.delete(id); }, IPC_TIMEOUT_MS); timeout.unref(); - process.addListener('message', messageHandler as () => void); - + this.#handlers.set(id, responseHandler); this.#sendMessage(request, (e: Error) => { if (e) { reject(e); diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index ede70bfbca..b8e0f5aef8 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/backend-dynamic-feature-service +## 0.2.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-app-api@0.8.1-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-events-backend@0.3.10-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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..400c55c009 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -33,7 +33,7 @@ import { ServiceRef } from '@backstage/backend-plugin-api'; import { TaskRunner } from '@backstage/backend-tasks'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { TokenManager } from '@backstage/backend-common'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public (undocumented) export interface BackendDynamicPlugin extends BaseDynamicPlugin { @@ -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) @@ -216,7 +221,7 @@ export type LegacyPluginEnvironment = { cache: PluginCacheManager; database: PluginDatabaseManager; config: Config; - reader: UrlReader; + reader: UrlReaderService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; permissions: PermissionEvaluator; diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 1282d39d6c..56474e4a65 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,24 +1,29 @@ { "name": "@backstage/backend-dynamic-feature-service", + "version": "0.2.16-next.3", "description": "Backstage dynamic feature service", - "version": "0.2.16-next.1", - "main": "src/index.ts", - "types": "src/index.ts", + "backstage": { + "role": "node-library" + }, "publishConfig": { "access": "public" }, + "keywords": [ + "backstage" + ], + "homepage": "https://backstage.io", "repository": { "type": "git", "url": "https://github.com/backstage/backstage", "directory": "packages/backend-dynamic-feature-service" }, - "backstage": { - "role": "node-library" - }, + "license": "Apache-2.0", "exports": { ".": "./src/index.ts", "./package.json": "./package.json" }, + "main": "src/index.ts", + "types": "src/index.ts", "typesVersions": { "*": { "package.json": [ @@ -26,23 +31,23 @@ ] } }, - "homepage": "https://backstage.io", - "keywords": [ - "backstage" + "files": [ + "dist", + "config.d.ts" ], - "license": "Apache-2.0", "scripts": { "build": "backstage-cli package build", + "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", - "clean": "backstage-cli package clean", - "start": "backstage-cli package start" + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/cli-common": "workspace:^", @@ -73,9 +78,5 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "wait-for-expect": "^3.0.2" - }, - "files": [ - "dist", - "config.d.ts" - ] + } } diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts index 8531624509..9c45cc53f6 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts @@ -37,15 +37,13 @@ import { import { ScannedPluginManifest, ScannedPluginPackage } from '../scanner/types'; import { randomUUID } from 'crypto'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { - createSpecializedBackend, - rootLifecycleServiceFactory, -} from '@backstage/backend-app-api'; +import { createSpecializedBackend } from '@backstage/backend-app-api'; import { ConfigSources } from '@backstage/config-loader'; import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils'; import { PluginScanner } from '../scanner/plugin-scanner'; import { findPaths } from '@backstage/cli-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; +import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; describe('backend-dynamic-feature-service', () => { const mockDir = createMockDirectory(); diff --git a/packages/backend-dynamic-feature-service/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts index 60b2eb831e..0234a8b66e 100644 --- a/packages/backend-dynamic-feature-service/src/manager/types.ts +++ b/packages/backend-dynamic-feature-service/src/manager/types.ts @@ -21,7 +21,6 @@ import { PluginDatabaseManager, PluginEndpointDiscovery, TokenManager, - UrlReader, } from '@backstage/backend-common'; import { Router } from 'express'; import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; @@ -33,7 +32,10 @@ import { HttpPostIngressOptions, } from '@backstage/plugin-events-node'; -import { BackendFeature } from '@backstage/backend-plugin-api'; +import { + BackendFeature, + UrlReaderService, +} from '@backstage/backend-plugin-api'; import { PackagePlatform, PackageRole } from '@backstage/cli-node'; import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; @@ -58,7 +60,7 @@ export type LegacyPluginEnvironment = { cache: PluginCacheManager; database: PluginDatabaseManager; config: Config; - reader: UrlReader; + reader: UrlReaderService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; permissions: PermissionEvaluator; diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts index 6681a65a75..00d5c85e6e 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts @@ -18,9 +18,9 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; +import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; import { transports, format } from 'winston'; -import { createConfigSecretEnumerator } from '@backstage/backend-app-api'; +import { createConfigSecretEnumerator } from '@backstage/backend-common'; import { loadConfigSchema } from '@backstage/config-loader'; import { getPackages } from '@manypkg/get-packages'; import { dynamicPluginsSchemasServiceRef } from './schemas'; diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index f036362cd2..ab180a8062 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,87 @@ # example-backend-legacy +## 0.2.101-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.3 + - @backstage/plugin-techdocs-backend@1.10.10-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-app-backend@0.3.72-next.3 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-devtools-backend@0.3.9-next.3 + - @backstage/plugin-events-backend@0.3.10-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-kubernetes-backend@0.18.4-next.3 + - @backstage/plugin-permission-backend@0.5.47-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-proxy-backend@0.5.4-next.3 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.3 + - @backstage/plugin-search-backend@1.5.15-next.3 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.3 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.3 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.3 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-signals-backend@0.1.9-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## 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..4fee66d925 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.3", "backstage": { "role": "backend" }, @@ -29,6 +29,8 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", diff --git a/packages/backend-legacy/src/index.ts b/packages/backend-legacy/src/index.ts index 33520cba03..e422a67267 100644 --- a/packages/backend-legacy/src/index.ts +++ b/packages/backend-legacy/src/index.ts @@ -32,7 +32,6 @@ import { loadBackendConfig, notFoundHandler, ServerTokenManager, - UrlReaders, useHotMemoize, } from '@backstage/backend-common'; import { TaskScheduler } from '@backstage/backend-tasks'; @@ -57,6 +56,7 @@ import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { DefaultEventBroker } from '@backstage/plugin-events-backend'; import { DefaultEventsService } from '@backstage/plugin-events-node'; import { DefaultSignalsService } from '@backstage/plugin-signals-node'; +import { UrlReaders } from '@backstage/backend-defaults/urlReader'; function makeCreateEnv(config: Config) { const root = getRootLogger(); diff --git a/packages/backend-legacy/src/types.ts b/packages/backend-legacy/src/types.ts index a75b1620c4..b2c0f58ee9 100644 --- a/packages/backend-legacy/src/types.ts +++ b/packages/backend-legacy/src/types.ts @@ -21,20 +21,20 @@ import { PluginDatabaseManager, PluginEndpointDiscovery, TokenManager, - UrlReader, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { EventBroker, EventsService } from '@backstage/plugin-events-node'; import { SignalsService } from '@backstage/plugin-signals-node'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; export type PluginEnvironment = { logger: Logger; cache: PluginCacheManager; database: PluginDatabaseManager; config: Config; - reader: UrlReader; + reader: UrlReaderService; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; permissions: PermissionEvaluator; diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index 37bb91f625..f7a238f712 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/backend-openapi-utils +## 0.1.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/errors@1.2.4 + +## 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..6125fa82ee 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.3", "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..6f4ed70318 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,112 @@ # @backstage/backend-plugin-api +## 0.8.0-next.3 + +### Patch Changes + +- ddde5fe: Fixed a type issue where plugin and modules depending on multiton services would not receive the correct type. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 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..a4a97faaa9 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.3", "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-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts similarity index 56% rename from packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts rename to packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts index b29a589438..825063e845 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts @@ -14,22 +14,14 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { HostDiscovery } from './HostDiscovery'; +import { createExtensionPoint } from './createExtensionPoint'; -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/discovery` instead. - */ -export const discoveryServiceFactory = createServiceFactory({ - service: coreServices.discovery, - deps: { - config: coreServices.rootConfig, - }, - async factory({ config }) { - return HostDiscovery.fromConfig(config); - }, +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-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts similarity index 67% rename from packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts rename to packages/backend-plugin-api/src/wiring/describeParentCallSite.ts index 988b907b4c..35603e33b0 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -14,11 +14,7 @@ * 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 -import { authServiceFactory as _authServiceFactory } from '../../../../../backend-defaults/src/entrypoints/auth'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/auth` instead. - */ -export const authServiceFactory = _authServiceFactory; +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..d4c9c3a59c 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/backend-tasks +## 0.5.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 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..8b815be190 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.3", "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..39d176e8a9 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/backend-test-utils +## 0.4.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/backend-app-api@0.8.1-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..f2a925c0ff 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -19,7 +19,7 @@ import { CacheService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { EventsService } from '@backstage/plugin-events-node'; -import { ExtendedHttpServer } from '@backstage/backend-app-api'; +import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; @@ -55,9 +55,6 @@ export interface CreateMockDirectoryOptions { mockOsTmpDir?: boolean; } -// @public @deprecated (undocumented) -export function isDockerDisabledForTests(): boolean; - // @public (undocumented) export namespace mockCredentials { export function limitedUser( @@ -143,9 +140,6 @@ export interface MockDirectoryContentOptions { shouldReadAsText?: boolean | ((path: string, buffer: Buffer) => boolean); } -// @public @deprecated (undocumented) -export type MockDirectoryOptions = CreateMockDirectoryOptions; - // @public (undocumented) export namespace mockServices { // (undocumented) @@ -156,7 +150,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 +164,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 +178,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 +194,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 +208,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 +227,8 @@ export namespace mockServices { export namespace httpAuth { const factory: ((options?: { defaultCredentials?: BackstageCredentials; - }) => ServiceFactory) & - ServiceFactory; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -218,7 +237,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 +253,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 +267,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 +281,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 +295,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 +315,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 +347,8 @@ export namespace mockServices { const // (undocumented) factory: (( options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory) & - ServiceFactory; + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -300,7 +357,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 +377,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 +389,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 +405,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 +419,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 +435,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 +456,30 @@ 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; - // @deprecated - get( - ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; - getService( - service: ServiceRef, + ): ServiceFactoryTester; + 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 @@ -408,13 +498,6 @@ export type ServiceMock = { : TService[Key]; }; -// @public @deprecated (undocumented) -export function setupRequestMockHandlers(worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; -}): void; - // @public (undocumented) export function startTestBackend( options: TestBackendOptions, diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index bb0786b02c..5373c62794 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.3", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" diff --git a/packages/backend-test-utils/src/deprecated.ts b/packages/backend-test-utils/src/deprecated.ts deleted file mode 100644 index 9790580a9c..0000000000 --- a/packages/backend-test-utils/src/deprecated.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 './msw'; -import { CreateMockDirectoryOptions } from './filesystem'; -import { isDockerDisabledForTests as _isDockerDisabledForTests } from './util'; - -/** - * @public - * @deprecated Use `CreateMockDirectoryOptions` from `@backstage/backend-test-utils` instead. - */ -export type MockDirectoryOptions = CreateMockDirectoryOptions; - -/** - * @public - * @deprecated Use `registerMswTestHooks` from `@backstage/backend-test-utils` instead. - */ -export function setupRequestMockHandlers(worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; -}): void { - registerMswTestHooks(worker); -} - -/** - * @public - * @deprecated This is an internal function and will no longer be exported from this package. - */ -export function isDockerDisabledForTests(): boolean { - return _isDockerDisabledForTests(); -} diff --git a/packages/backend-test-utils/src/index.ts b/packages/backend-test-utils/src/index.ts index 4124a861f4..d1d80bf387 100644 --- a/packages/backend-test-utils/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -20,7 +20,6 @@ * @packageDocumentation */ -export * from './deprecated'; export * from './cache'; export * from './database'; export * from './msw'; diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index aea0d9d270..83a6a42307 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,24 +75,13 @@ export class ServiceFactoryTester { } private constructor( - subject: ServiceRef, + subject: ServiceRef, registry: ServiceRegistry, ) { this.#subject = subject; this.#registry = registry; } - /** - * Returns the service instance for the subject. - * - * @deprecated Use `getSubject` instead. - */ - async get( - ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { - return this.getSubject(...args); - } - /** * Returns the service instance for the subject. * @@ -97,9 +94,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 +107,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..6b4110d4de 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { - Backend, - createSpecializedBackend, - MiddlewareFactory, - createHttpServer, - ExtendedHttpServer, - HostDiscovery, - DefaultRootHttpRouter, -} from '@backstage/backend-app-api'; +import { Backend, createSpecializedBackend } from '@backstage/backend-app-api'; import { createServiceFactory, BackendFeature, @@ -36,8 +28,18 @@ 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 { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter'; +import { + InternalBackendFeature, + InternalBackendRegistrations, +} from '@backstage/backend-plugin-api/src/wiring/types'; +import { + DefaultRootHttpRouter, + ExtendedHttpServer, + MiddlewareFactory, + createHealthRouter, + createHttpServer, +} from '@backstage/backend-defaults/rootHttpRouter'; +import { HostDiscovery } from '@backstage/backend-defaults/discovery'; /** @public */ export interface TestBackendOptions { @@ -94,7 +96,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 +139,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 +352,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..c2e4b76dab 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,79 @@ # example-backend +## 0.0.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.3 + - @backstage/plugin-notifications-backend@0.3.4-next.3 + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-techdocs-backend@1.10.10-next.3 + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/plugin-app-backend@0.3.72-next.3 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.3 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.3 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.3 + - @backstage/plugin-devtools-backend@0.3.9-next.3 + - @backstage/plugin-kubernetes-backend@0.18.4-next.3 + - @backstage/plugin-permission-backend@0.5.47-next.3 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-proxy-backend@0.5.4-next.3 + - @backstage/plugin-search-backend@1.5.15-next.3 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.3 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-signals-backend@0.1.9-next.3 + +## 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..4e2007bc00 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.3", "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-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index da7b16dce1..361722fbd1 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-client +## 1.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + ## 1.6.5 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index c9e7f25723..221e25c668 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "1.6.5", + "version": "1.6.6-next.0", "description": "An isomorphic client for the catalog backend", "backstage": { "role": "common-library" diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index f47465a03e..360a0b927e 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/catalog-model +## 1.6.0-next.0 + +### Minor Changes + +- 34fa803: Introduce an optional spec.type attribute on the Domain and System entity kinds + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.5.0 ### Minor Changes 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/package.json b/packages/catalog-model/package.json index 5a60531cb2..e4224cdff9 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "1.5.0", + "version": "1.6.0-next.0", "description": "Types and validators that help describe the model of a Backstage Catalog", "backstage": { "role": "common-library" 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..e8092c5aae 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/cli +## 0.27.0-next.4 + +### Patch Changes + +- 6d898d8: Switched the `process` polyfill to use `require.resolve` for greater compatability. +- 2ced236: Updated dependency `@module-federation/enhanced` to `0.3.1` +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.14.0-next.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 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 79740ea4fa..de38b0c656 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.4", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -53,7 +53,7 @@ "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", - "@module-federation/enhanced": "^0.1.19", + "@module-federation/enhanced": "^0.3.5", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", "@octokit/oauth-app": "^4.2.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 1b49af9cf0..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" diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index 587d78242b..d5570ef632 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/core-compat-api +## 0.2.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## 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..560a547af5 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.3", "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/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index aa3beee679..f8b40a561c 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -31,7 +31,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { collectLegacyRoutes } from './collectLegacyRoutes'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalBackstagePlugin } from '../../frontend-plugin-api/src/wiring/createPlugin'; +import { toInternalBackstagePlugin } from '../../frontend-plugin-api/src/wiring/createFrontendPlugin'; import { createPlugin, createRoutableExtension, @@ -70,7 +70,7 @@ describe('collectLegacyRoutes', () => { id: 'page:score-card', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'score-board' }, + defaultConfig: {}, }, { id: 'api:plugin.scoringdata.service', @@ -86,7 +86,7 @@ describe('collectLegacyRoutes', () => { id: 'page:stackstorm', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'stackstorm' }, + defaultConfig: {}, }, { id: 'api:plugin.stackstorm.service', @@ -102,13 +102,13 @@ describe('collectLegacyRoutes', () => { id: 'page:puppetDb', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'puppetdb' }, + defaultConfig: {}, }, { id: 'page:puppetDb/1', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'puppetdb' }, + defaultConfig: {}, }, { id: 'api:plugin.puppetdb.service', @@ -173,12 +173,12 @@ describe('collectLegacyRoutes', () => { id: 'page:catalog', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'catalog' }, + defaultConfig: {}, }, { id: 'page:catalog/1', attachTo: { id: 'app/routes', input: 'routes' }, - defaultConfig: { path: 'catalog/:namespace/:kind/:name' }, + defaultConfig: {}, disabled: false, }, { diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index 3523b0a40b..c5cb13b6e1 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -24,11 +24,11 @@ import { BackstagePlugin, ExtensionDefinition, coreExtensionData, - createApiExtension, createExtension, createExtensionInput, - createPageExtension, - createPlugin, + createFrontendPlugin, + ApiBlueprint, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import React, { Children, ReactNode, isValidElement } from 'react'; import { Route, Routes } from 'react-router-dom'; @@ -80,19 +80,24 @@ function makeRoutingShimExtension(options: { name, attachTo: { id: parentExtensionId, input: 'childRoutingShims' }, inputs: { - childRoutingShims: createExtensionInput({ - routePath: coreExtensionData.routePath.optional(), - routeRef: coreExtensionData.routeRef.optional(), - }), + childRoutingShims: createExtensionInput([ + coreExtensionData.routePath.optional(), + coreExtensionData.routeRef.optional(), + ]), }, - output: { - routePath: coreExtensionData.routePath.optional(), - routeRef: coreExtensionData.routeRef.optional(), + output: [ + coreExtensionData.routePath.optional(), + coreExtensionData.routeRef.optional(), + ], + *factory() { + if (routePath !== undefined) { + yield coreExtensionData.routePath(routePath); + } + + if (routeRef) { + yield coreExtensionData.routeRef(convertLegacyRouteRef(routeRef)); + } }, - factory: () => ({ - routePath, - routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, - }), }); } @@ -214,28 +219,33 @@ export function collectLegacyRoutes( }`; extensions.push( - createPageExtension({ + PageBlueprint.makeWithOverrides({ name: pageExtensionName, - defaultPath: path[0] === '/' ? path.slice(1) : path, - routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, inputs: { - childRoutingShims: createExtensionInput({ - routePath: coreExtensionData.routePath.optional(), - routeRef: coreExtensionData.routeRef.optional(), - }), + childRoutingShims: createExtensionInput([ + coreExtensionData.routePath.optional(), + coreExtensionData.routeRef.optional(), + ]), + }, + factory(originalFactory, { inputs: _inputs }) { + // todo(blam): why do we not use the inputs here? + return originalFactory({ + defaultPath: path[0] === '/' ? path.slice(1) : path, + routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, + loader: async () => + compatWrapper( + route.props.children ? ( + + + + + + ) : ( + routeElement + ), + ), + }); }, - loader: async () => - compatWrapper( - route.props.children ? ( - - - - - - ) : ( - routeElement - ), - ), }), ); @@ -253,12 +263,12 @@ export function collectLegacyRoutes( ); return Array.from(pluginExtensions).map(([plugin, extensions]) => - createPlugin({ + createFrontendPlugin({ id: plugin.getId(), extensions: [ ...extensions, ...Array.from(plugin.getApis()).map(factory => - createApiExtension({ factory }), + ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }), ), ], routes: convertLegacyRouteRefs(plugin.routes ?? {}), diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 288f3f900b..445f42124f 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -21,7 +21,7 @@ import { AppContextProvider } from '../../../core-app-api/src/app/AppContext'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { RouteResolver } from '../../../core-plugin-api/src/routing/useRouteRef'; import { - createPlugin as createNewPlugin, + createFrontendPlugin as createNewPlugin, BackstagePlugin as NewBackstagePlugin, appTreeApiRef, componentsApiRef, @@ -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..6357f13906 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', () => { @@ -38,9 +45,7 @@ describe('BackwardsCompatProvider', () => { createExtensionTester( createExtension({ attachTo: { id: 'ignored', input: 'ignored' }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory() { function Component() { const app = useApp(); @@ -59,9 +64,7 @@ describe('BackwardsCompatProvider', () => { ); } - return { - element: compatWrapper(), - }; + return [coreExtensionData.reactElement(compatWrapper())]; }, }), ).render(); @@ -74,14 +77,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/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx index 2e7be66915..ea84c06ba9 100644 --- a/packages/core-compat-api/src/convertLegacyApp.test.tsx +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -62,7 +62,7 @@ describe('convertLegacyApp', () => { id: 'page:score-card', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'score-board' }, + defaultConfig: {}, }, { id: 'api:plugin.scoringdata.service', @@ -78,7 +78,7 @@ describe('convertLegacyApp', () => { id: 'page:stackstorm', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'stackstorm' }, + defaultConfig: {}, }, { id: 'api:plugin.stackstorm.service', @@ -94,13 +94,13 @@ describe('convertLegacyApp', () => { id: 'page:puppetDb', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'puppetdb' }, + defaultConfig: {}, }, { id: 'page:puppetDb/1', attachTo: { id: 'app/routes', input: 'routes' }, disabled: false, - defaultConfig: { path: 'puppetdb' }, + defaultConfig: {}, }, { id: 'api:plugin.puppetdb.service', diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 72ce4b5e61..ffeab3bdc6 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -107,33 +107,30 @@ export function convertLegacyApp( name: 'layout', attachTo: { id: 'app', input: 'root' }, inputs: { - content: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, - ), - }, - output: { - element: coreExtensionData.reactElement, + content: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), }, + output: [coreExtensionData.reactElement], factory({ inputs }) { // Clone the root element, this replaces the FlatRoutes declared in the app with out content input - return { - element: React.cloneElement( - rootEl, - undefined, - inputs.content.output.element, + return [ + coreExtensionData.reactElement( + React.cloneElement( + rootEl, + undefined, + inputs.content.get(coreExtensionData.reactElement), + ), ), - }; + ]; }, }); const CoreNavOverride = createExtension({ namespace: 'app', name: 'nav', attachTo: { id: 'app/layout', input: 'nav' }, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], disabled: true, }); 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/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx index 98fca7c6ef..df0d256312 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx @@ -34,7 +34,7 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; */ export type ProxiedSignInPageProps = SignInPageProps & { /** - * The provider to use, e.g. "gcp-iap" or "awsalb". This must correspond to + * The provider to use, e.g. "gcpIap" or "awsalb". This must correspond to * a properly configured auth provider ID in the auth backend. */ provider: string; diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 85587de268..29ebe72296 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/create-app +## 0.5.18-next.4 + +### Patch Changes + +- bfeba46: Included permission config and enabled it out of the box +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 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 df46c8ffde..76934282ca 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.4", "publishConfig": { "access": "public" }, 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..a5e1688e6d 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/dev-utils +## 1.0.37-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## 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..255cc478b0 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.3", "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..23337c9ca3 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,23 @@ # e2e-test +## 0.2.19-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.4 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 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..0e11195ba1 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.4", "private": true, "backstage": { "role": "cli" diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index 8593bb38e5..f26e52bccd 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/frontend-app-api +## 0.7.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @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.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..4c82731c4c 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.3", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx index 7d62103727..e2c7581e63 100644 --- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx @@ -40,14 +40,8 @@ const baseOverrides = createExtensionOverrides({ namespace: 'app', name: 'root', attachTo: { id: 'app', input: 'root' }, - output: { - element: coreExtensionData.reactElement, - }, - factory() { - return { - element:
root
, - }; - }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
root
)], }), ], }); diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index 7794aceb79..ba6c5f65f1 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -18,51 +18,38 @@ import React from 'react'; import { ExtensionBoundary, coreExtensionData, - createApiExtension, createComponentExtension, createExtension, createExtensionInput, - createThemeExtension, - createTranslationExtension, IconBundleBlueprint, + ThemeBlueprint, + ApiBlueprint, + TranslationBlueprint, } from '@backstage/frontend-plugin-api'; export const App = createExtension({ namespace: 'app', attachTo: { id: 'root', input: 'default' }, // ignored inputs: { - apis: createExtensionInput({ - api: createApiExtension.factoryDataRef, + apis: createExtensionInput([ApiBlueprint.dataRefs.factory]), + themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]), + components: createExtensionInput([ + createComponentExtension.componentDataRef, + ]), + translations: createExtensionInput([ + TranslationBlueprint.dataRefs.translation, + ]), + icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]), + root: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, }), - themes: createExtensionInput({ - theme: createThemeExtension.themeDataRef, - }), - components: createExtensionInput({ - component: createComponentExtension.componentDataRef, - }), - translations: createExtensionInput({ - translation: createTranslationExtension.translationDataRef, - }), - icons: createExtensionInput({ - icon: IconBundleBlueprint.dataRefs.icons, - }), - root: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, + }, + output: [coreExtensionData.reactElement], + factory: ({ node, inputs }) => [ + coreExtensionData.reactElement( + + {inputs.root.get(coreExtensionData.reactElement)} + , ), - }, - output: { - root: coreExtensionData.reactElement, - }, - factory({ node, inputs }) { - return { - root: ( - - {inputs.root.output.element} - - ), - }; - }, + ], }); diff --git a/packages/frontend-app-api/src/extensions/AppLayout.tsx b/packages/frontend-app-api/src/extensions/AppLayout.tsx index 7be0029c25..9fadba8a93 100644 --- a/packages/frontend-app-api/src/extensions/AppLayout.tsx +++ b/packages/frontend-app-api/src/extensions/AppLayout.tsx @@ -27,30 +27,20 @@ export const AppLayout = createExtension({ name: 'layout', attachTo: { id: 'app/root', input: 'children' }, inputs: { - nav: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, + nav: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + content: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement], + factory: ({ inputs }) => [ + coreExtensionData.reactElement( + + {inputs.nav.get(coreExtensionData.reactElement)} + {inputs.content.get(coreExtensionData.reactElement)} + , ), - content: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, - ), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - {inputs.nav.output.element} - {inputs.content.output.element} - - ), - }; - }, + ], }); diff --git a/packages/frontend-app-api/src/extensions/AppNav.tsx b/packages/frontend-app-api/src/extensions/AppNav.tsx index 761c6d5a24..49c95e84f2 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({ @@ -83,33 +86,27 @@ export const AppNav = createExtension({ name: 'nav', attachTo: { id: 'app/layout', input: 'nav' }, inputs: { - items: createExtensionInput({ - target: createNavItemExtension.targetDataRef, + items: createExtensionInput([createNavItemExtension.targetDataRef]), + logos: createExtensionInput([createNavLogoExtension.logoElementsDataRef], { + singleton: true, + optional: true, }), - logos: createExtensionInput( - { - elements: createNavLogoExtension.logoElementsDataRef, - }, - { - singleton: true, - optional: true, - }, + }, + output: [coreExtensionData.reactElement], + factory: ({ inputs }) => [ + coreExtensionData.reactElement( + + + + {inputs.items.map((item, index) => ( + + ))} + , ), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - - - {inputs.items.map((item, index) => ( - - ))} - - ), - }; - }, + ], }); diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index f3a962dd10..a158386d04 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -23,12 +23,12 @@ import React, { useState, } from 'react'; import { + AppRootWrapperBlueprint, + RouterBlueprint, + SignInPageBlueprint, coreExtensionData, - createAppRootWrapperExtension, createExtension, createExtensionInput, - createRouterExtension, - createSignInPageExtension, } from '@backstage/frontend-plugin-api'; import { IdentityApi, @@ -48,52 +48,54 @@ export const AppRoot = createExtension({ name: 'root', attachTo: { id: 'app', input: 'root' }, inputs: { - router: createExtensionInput( - { component: createRouterExtension.componentDataRef }, - { singleton: true, optional: true }, - ), - signInPage: createExtensionInput( - { component: createSignInPageExtension.componentDataRef }, - { singleton: true, optional: true }, - ), - children: createExtensionInput( - { element: coreExtensionData.reactElement }, - { singleton: true }, - ), - elements: createExtensionInput({ - element: coreExtensionData.reactElement, + router: createExtensionInput([RouterBlueprint.dataRefs.component], { + singleton: true, + optional: true, }), - wrappers: createExtensionInput({ - component: createAppRootWrapperExtension.componentDataRef, + signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], { + singleton: true, + optional: true, }), + children: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + elements: createExtensionInput([coreExtensionData.reactElement]), + wrappers: createExtensionInput([ + AppRootWrapperBlueprint.dataRefs.component, + ]), }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory({ inputs }) { let content: React.ReactNode = ( <> {inputs.elements.map(el => ( - {el.output.element} + + {el.get(coreExtensionData.reactElement)} + ))} - {inputs.children.output.element} + {inputs.children.get(coreExtensionData.reactElement)} ); for (const wrapper of inputs.wrappers) { - content = {content}; + const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component); + content = {content}; } - return { - element: ( + return [ + coreExtensionData.reactElement( {content} - + , ), - }; + ]; }, }); diff --git a/packages/frontend-app-api/src/extensions/AppRoutes.tsx b/packages/frontend-app-api/src/extensions/AppRoutes.tsx index 38a9291755..6ef7e2a271 100644 --- a/packages/frontend-app-api/src/extensions/AppRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoutes.tsx @@ -29,15 +29,13 @@ export const AppRoutes = createExtension({ name: 'routes', attachTo: { id: 'app/layout', input: 'content' }, inputs: { - routes: createExtensionInput({ - path: coreExtensionData.routePath, - ref: coreExtensionData.routeRef.optional(), - element: coreExtensionData.reactElement, - }), - }, - output: { - element: coreExtensionData.reactElement, + routes: createExtensionInput([ + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + coreExtensionData.reactElement, + ]), }, + output: [coreExtensionData.reactElement], factory({ inputs }) { const Routes = () => { const NotFoundErrorPage = useComponentRef( @@ -46,8 +44,8 @@ export const AppRoutes = createExtension({ const element = useRoutes([ ...inputs.routes.map(route => ({ - path: `${route.output.path}/*`, - element: route.output.element, + path: `${route.get(coreExtensionData.routePath)}/*`, + element: route.get(coreExtensionData.reactElement), })), { path: '*', @@ -58,8 +56,6 @@ export const AppRoutes = createExtension({ return element; }; - return { - element: , - }; + return [coreExtensionData.reactElement()]; }, }); diff --git a/packages/frontend-app-api/src/extensions/elements.tsx b/packages/frontend-app-api/src/extensions/elements.tsx index 0a764583bb..1ab7f97ba2 100644 --- a/packages/frontend-app-api/src/extensions/elements.tsx +++ b/packages/frontend-app-api/src/extensions/elements.tsx @@ -15,31 +15,36 @@ */ import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; -import { - createAppRootElementExtension, - createSchemaFromZod, -} from '@backstage/frontend-plugin-api'; +import { AppRootElementBlueprint } from '@backstage/frontend-plugin-api'; import React from 'react'; -export const oauthRequestDialogAppRootElement = createAppRootElementExtension({ +export const oauthRequestDialogAppRootElement = AppRootElementBlueprint.make({ namespace: 'app', name: 'oauth-request-dialog', - element: , + params: { + element: , + }, }); -export const alertDisplayAppRootElement = createAppRootElementExtension({ - namespace: 'app', - name: 'alert-display', - configSchema: createSchemaFromZod(z => - z.object({ - transientTimeoutMs: z.number().default(5000), - anchorOrigin: z - .object({ - vertical: z.enum(['top', 'bottom']).default('top'), - horizontal: z.enum(['left', 'center', 'right']).default('center'), - }) - .default({}), - }), - ), - element: ({ config }) => , -}); +export const alertDisplayAppRootElement = + AppRootElementBlueprint.makeWithOverrides({ + namespace: 'app', + name: 'alert-display', + config: { + schema: { + transientTimeoutMs: z => z.number().default(5000), + anchorOrigin: z => + z + .object({ + vertical: z.enum(['top', 'bottom']).default('top'), + horizontal: z.enum(['left', 'center', 'right']).default('center'), + }) + .default({}), + }, + }, + factory: (originalFactory, { config }) => { + return originalFactory({ + element: () => , + }); + }, + }); diff --git a/packages/frontend-app-api/src/extensions/themes.tsx b/packages/frontend-app-api/src/extensions/themes.tsx index ea11b6a638..a4fd0b253e 100644 --- a/packages/frontend-app-api/src/extensions/themes.tsx +++ b/packages/frontend-app-api/src/extensions/themes.tsx @@ -21,24 +21,34 @@ import { } from '@backstage/theme'; import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; -import { createThemeExtension } from '@backstage/frontend-plugin-api'; +import { ThemeBlueprint } from '@backstage/frontend-plugin-api'; -export const LightTheme = createThemeExtension({ - id: 'light', - title: 'Light Theme', - variant: 'light', - icon: , - Provider: ({ children }) => ( - - ), +export const LightTheme = ThemeBlueprint.make({ + name: 'light', + params: { + theme: { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + Provider: ({ children }) => ( + + ), + }, + }, }); -export const DarkTheme = createThemeExtension({ - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - icon: , - Provider: ({ children }) => ( - - ), +export const DarkTheme = ThemeBlueprint.make({ + name: 'dark', + params: { + theme: { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + ), + }, + }, }); 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/collectRouteIds.test.ts b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts index 6b9fdecf70..d74ab43e33 100644 --- a/packages/frontend-app-api/src/routing/collectRouteIds.test.ts +++ b/packages/frontend-app-api/src/routing/collectRouteIds.test.ts @@ -17,7 +17,7 @@ import { createRouteRef, createExternalRouteRef, - createPlugin, + createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { collectRouteIds } from './collectRouteIds'; @@ -34,7 +34,11 @@ describe('collectRouteIds', () => { ); const collected = collectRouteIds([ - createPlugin({ id: 'test', routes: { ref }, externalRoutes: { extRef } }), + createFrontendPlugin({ + id: 'test', + routes: { ref }, + externalRoutes: { extRef }, + }), ]); expect(Object.fromEntries(collected.routes)).toEqual({ 'test.ref': ref, diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index bdab2781e0..51d26ec51b 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -25,7 +25,7 @@ import { coreExtensionData, createExtension, createExtensionInput, - createPlugin, + createFrontendPlugin, createRouteRef, } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; @@ -50,28 +50,30 @@ function createTestExtension(options: { attachTo: options.parent ? { id: `test/${options.parent}`, input: 'children' } : { id: 'app/routes', input: 'routes' }, - output: { - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath.optional(), - routeRef: coreExtensionData.routeRef.optional(), - }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath.optional(), + coreExtensionData.routeRef.optional(), + ], inputs: { - children: createExtensionInput({ - element: coreExtensionData.reactElement, - }), + children: createExtensionInput([coreExtensionData.reactElement]), }, - factory() { - return { - path: options.path, - routeRef: options.routeRef, - element: React.createElement('div'), - }; + *factory() { + if (options.path !== undefined) { + yield coreExtensionData.routePath(options.path); + } + + if (options.routeRef) { + yield coreExtensionData.routeRef(options.routeRef); + } + + yield coreExtensionData.reactElement(React.createElement('div')); }, }); } function routeInfoFromExtensions(extensions: ExtensionDefinition[]) { - const plugin = createPlugin({ + const plugin = createFrontendPlugin({ id: 'test', extensions, }); 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/createAppTree.test.ts b/packages/frontend-app-api/src/tree/createAppTree.test.ts index 9ee58fe2d2..f5df5270cd 100644 --- a/packages/frontend-app-api/src/tree/createAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/createAppTree.test.ts @@ -17,7 +17,7 @@ import { createExtension, createExtensionOverrides, - createPlugin, + createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; import { createAppTree } from './createAppTree'; @@ -25,8 +25,8 @@ import { createAppTree } from './createAppTree'; const extBase = { id: 'test', attachTo: { id: 'app', input: 'root' }, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }; describe('createAppTree', () => { @@ -41,7 +41,7 @@ describe('createAppTree', () => { }, }); const features = [ - createPlugin({ + createFrontendPlugin({ id: 'plugin', extensions: [], }), @@ -60,8 +60,8 @@ describe('createAppTree', () => { name: 'app', attachTo: { id: 'app/routes', input: 'route' }, inputs: {}, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }), ], }), 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/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 65626e0be1..c70e351105 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -92,6 +92,16 @@ function resolveInputDataContainer( get(ref) { return dataMap.get(ref.id); }, + *[Symbol.iterator]() { + for (const [id, value] of dataMap) { + // TODO: Would be better to be able to create a new instance using the ref here instead + yield { + $$type: '@backstage/ExtensionDataValue', + id, + value, + }; + } + }, } as { node: AppNode } & ExtensionDataContainer; } diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 8af2aef070..a732b327b6 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -16,7 +16,7 @@ import { createExtensionOverrides, - createPlugin, + createFrontendPlugin, Extension, ExtensionDefinition, } from '@backstage/frontend-plugin-api'; @@ -48,6 +48,7 @@ function makeExtDef( name, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + override: () => ({} as ExtensionDefinition), } as ExtensionDefinition; } @@ -97,7 +98,10 @@ describe('resolveAppNodeSpecs', () => { it('should override attachment points', () => { const b = makeExt('b'); - const pluginA = createPlugin({ id: 'test', extensions: [makeExtDef('a')] }); + const pluginA = createFrontendPlugin({ + id: 'test', + extensions: [makeExtDef('a')], + }); expect( resolveAppNodeSpecs({ features: [pluginA], @@ -129,7 +133,7 @@ describe('resolveAppNodeSpecs', () => { it('should fully override configuration and duplicate', () => { const a = makeExt('test/a'); const b = makeExt('test/b'); - const plugin = createPlugin({ + const plugin = createFrontendPlugin({ id: 'test', extensions: [makeExtDef('a'), makeExtDef('b')], }); @@ -177,7 +181,7 @@ describe('resolveAppNodeSpecs', () => { const b = makeExt('b', 'disabled'); expect( resolveAppNodeSpecs({ - features: [createPlugin({ id: 'empty', extensions: [] })], + features: [createFrontendPlugin({ id: 'empty', extensions: [] })], builtinExtensions: [a, b], parameters: [ { @@ -216,7 +220,7 @@ describe('resolveAppNodeSpecs', () => { const g = makeExt('g', 'disabled'); expect( resolveAppNodeSpecs({ - features: [createPlugin({ id: 'empty', extensions: [] })], + features: [createFrontendPlugin({ id: 'empty', extensions: [] })], builtinExtensions: [a, b, c, d, e, f, g], parameters: [ { id: 'e', disabled: false }, @@ -271,7 +275,7 @@ describe('resolveAppNodeSpecs', () => { }); it('should apply extension overrides', () => { - const plugin = createPlugin({ + const plugin = createFrontendPlugin({ id: 'test', extensions: [makeExtDef('a'), makeExtDef('b')], }); @@ -322,7 +326,7 @@ describe('resolveAppNodeSpecs', () => { it('should use order from configuration when rather than overrides', () => { const result = resolveAppNodeSpecs({ features: [ - createPlugin({ + createFrontendPlugin({ id: 'test', extensions: [ makeExtDef('a', 'disabled'), @@ -356,7 +360,10 @@ describe('resolveAppNodeSpecs', () => { expect(() => resolveAppNodeSpecs({ features: [ - createPlugin({ id: 'test', extensions: [makeExtDef('forbidden')] }), + createFrontendPlugin({ + id: 'test', + extensions: [makeExtDef('forbidden')], + }), ], builtinExtensions: [], parameters: [], diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index e31c70025d..0560066f6b 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -25,7 +25,7 @@ import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/w import { ExtensionParameters } from './readAppExtensionsConfig'; import { AppNodeSpec } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createPlugin'; +import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index d9b65555fe..740f05d5f7 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -23,8 +23,8 @@ const extension = resolveExtensionDefinition( createExtension({ name: 'test', attachTo: { id: 'nonexistent', input: 'nonexistent' }, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }), ) as Extension; diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 94254a2e01..c79d8eed76 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -20,9 +20,9 @@ import { coreExtensionData, createExtension, createExtensionOverrides, - createPageExtension, - createPlugin, - createThemeExtension, + PageBlueprint, + createFrontendPlugin, + ThemeBlueprint, } from '@backstage/frontend-plugin-api'; import { screen, waitFor } from '@testing-library/react'; import { CreateAppFeatureLoader, createApp } from './createApp'; @@ -44,14 +44,19 @@ describe('createApp', () => { }), }), features: [ - createPlugin({ + createFrontendPlugin({ id: 'test', extensions: [ - createThemeExtension({ - id: 'derp', - title: 'Derp', - variant: 'dark', - Provider: () =>
Derp
, + ThemeBlueprint.make({ + name: 'derp', + params: { + theme: { + id: 'derp', + title: 'Derp', + variant: 'dark', + Provider: () =>
Derp
, + }, + }, }), ], }), @@ -68,21 +73,25 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ config: new MockConfigApi({}) }), features: [ - createPlugin({ + createFrontendPlugin({ id: duplicatedFeatureId, extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
First Page
, + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
First Page
, + }, }), ], }), - createPlugin({ + createFrontendPlugin({ id: duplicatedFeatureId, extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
Last Page
, + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
Last Page
, + }, }), ], }), @@ -107,12 +116,14 @@ describe('createApp', () => { async load({ config }) { return { features: [ - createPlugin({ + createFrontendPlugin({ id: 'test', extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
{config.getString('key')}
, + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
{config.getString('key')}
, + }, }), ], }), @@ -163,14 +174,14 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ config: new MockConfigApi({}) }), features: [ - createPlugin({ + createFrontendPlugin({ id: 'test', featureFlags: [{ name: 'test-1' }], extensions: [ createExtension({ name: 'first', attachTo: { id: 'app', input: 'root' }, - output: { element: coreExtensionData.reactElement }, + output: [coreExtensionData.reactElement], factory() { const Component = () => { const flagsApi = useApi(featureFlagsApiRef); @@ -184,7 +195,7 @@ describe('createApp', () => { ); }; - return { element: }; + return [coreExtensionData.reactElement()]; }, }), ], @@ -197,8 +208,8 @@ describe('createApp', () => { name: 'root', attachTo: { id: 'app', input: 'root' }, disabled: true, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }), ], }), @@ -218,17 +229,19 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ config: new MockConfigApi({}) }), features: [ - createPlugin({ + createFrontendPlugin({ id: 'my-plugin', extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () => { - const Component = () => { - appTreeApi = useApi(appTreeApiRef); - return
My Plugin Page
; - }; - return ; + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () => { + const Component = () => { + appTreeApi = useApi(appTreeApiRef); + return
My Plugin Page
; + }; + return ; + }, }, }), ], @@ -250,7 +263,7 @@ describe('createApp', () => { content [ routes [ - + ] ] diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index d82d0ca782..ca42dce3e4 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -17,18 +17,18 @@ import React, { JSX, ReactNode } from 'react'; import { ConfigReader } from '@backstage/config'; import { + ApiBlueprint, AppTree, appTreeApiRef, componentsApiRef, coreExtensionData, - createApiExtension, - createThemeExtension, - createTranslationExtension, FrontendFeature, IconBundleBlueprint, iconsApiRef, RouteResolutionApi, routeResolutionApiRef, + ThemeBlueprint, + TranslationBlueprint, } from '@backstage/frontend-plugin-api'; import { App } from '../extensions/App'; import { AppRoutes } from '../extensions/AppRoutes'; @@ -100,7 +100,7 @@ import { import { InternalAppContext } from './InternalAppContext'; import { AppRoot } from '../extensions/AppRoot'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createPlugin'; +import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi'; @@ -110,7 +110,9 @@ import { stringifyError } from '@backstage/errors'; import { icons as defaultIcons } from '../../../app-defaults/src/defaults'; import { getBasePath } from '../routing/getBasePath'; -const DefaultApis = defaultApis.map(factory => createApiExtension({ factory })); +const DefaultApis = defaultApis.map(factory => + ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }), +); export const builtinExtensions = [ App, @@ -357,23 +359,21 @@ function createApiHolder( const pluginApis = tree.root.edges.attachments .get('apis') - ?.map(e => e.instance?.getData(createApiExtension.factoryDataRef)) + ?.map(e => e.instance?.getData(ApiBlueprint.dataRefs.factory)) .filter((x): x is AnyApiFactory => !!x) ?? []; const themeExtensions = tree.root.edges.attachments .get('themes') - ?.map(e => e.instance?.getData(createThemeExtension.themeDataRef)) + ?.map(e => e.instance?.getData(ThemeBlueprint.dataRefs.theme)) .filter((x): x is AppTheme => !!x) ?? []; const translationResources = tree.root.edges.attachments .get('translations') - ?.map(e => - e.instance?.getData(createTranslationExtension.translationDataRef), - ) + ?.map(e => e.instance?.getData(TranslationBlueprint.dataRefs.translation)) .filter( - (x): x is typeof createTranslationExtension.translationDataRef.T => !!x, + (x): x is typeof TranslationBlueprint.dataRefs.translation.T => !!x, ) ?? []; const extensionIcons = tree.root.edges.attachments diff --git a/packages/frontend-app-api/src/wiring/discovery.test.ts b/packages/frontend-app-api/src/wiring/discovery.test.ts index 6008ce6746..8570ca1bc7 100644 --- a/packages/frontend-app-api/src/wiring/discovery.test.ts +++ b/packages/frontend-app-api/src/wiring/discovery.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/frontend-plugin-api'; +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; import { getAvailableFeatures } from './discovery'; import { ConfigReader } from '@backstage/config'; @@ -42,7 +42,7 @@ describe('getAvailableFeatures', () => { }); it('should discover a plugin', () => { - const testPlugin = createPlugin({ id: 'test' }); + const testPlugin = createFrontendPlugin({ id: 'test' }); globalSpy.mockReturnValue({ modules: [{ default: testPlugin }], }); @@ -67,9 +67,9 @@ describe('getAvailableFeatures', () => { }); it('should discover multiple plugins', () => { - const test1Plugin = createPlugin({ id: 'test1' }); - const test2Plugin = createPlugin({ id: 'test2' }); - const test3Plugin = createPlugin({ id: 'test3' }); + const test1Plugin = createFrontendPlugin({ id: 'test1' }); + const test2Plugin = createFrontendPlugin({ id: 'test2' }); + const test3Plugin = createFrontendPlugin({ id: 'test3' }); globalSpy.mockReturnValue({ modules: [ { default: test1Plugin }, diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 40961b558f..d419a0a5cb 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,82 @@ # @backstage/frontend-plugin-api +## 0.7.0-next.3 + +### Patch Changes + +- 6f72c2b: Fixing issue with extension blueprints `inputs` merging. +- 99abb6b: Support overriding of plugin extensions using the new `plugin.withOverrides` method. + + ```tsx + import homePlugin from '@backstage/plugin-home'; + + export default homePlugin.withOverrides({ + extensions: [ + homePage.getExtension('page:home').override({ + *factory(originalFactory) { + yield* originalFactory(); + yield coreExtensionData.reactElement(

My custom home page

); + }, + }), + ], + }); + ``` + +- a65cfc8: 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. +- 34f1b2a: 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. +- 2d21599: 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)} + + ); + } + }); + + ``` + +- 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.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..c736f3b7d0 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,26 @@ 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; + // (undocumented) + withOverrides(options: { + extensions: Array>; + }): BackstagePlugin; } export { BackstageUserIdentity }; @@ -333,7 +407,7 @@ export interface ConfigurableExtensionDataRef< optional(): ConfigurableExtensionDataRef< TData, TId, - TData & { + TConfig & { optional: true; } >; @@ -376,7 +450,7 @@ export type CoreNotFoundErrorPageProps = { // @public (undocumented) export type CoreProgressProps = {}; -// @public (undocumented) +// @public @deprecated (undocumented) export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -396,7 +470,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 +494,7 @@ export { createApiFactory }; export { createApiRef }; -// @public +// @public @deprecated export function createAppRootElementExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -434,7 +516,7 @@ export function createAppRootElementExtension< }) => JSX_2.Element); }): ExtensionDefinition; -// @public +// @public @deprecated export function createAppRootWrapperExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -492,7 +574,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 +614,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 +653,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 +675,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 +698,12 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, - TInputs, - UExtraOutput, + string extends keyof TInputs ? {} : TInputs, string extends keyof TConfigSchema ? {} : { @@ -642,6 +721,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 +743,8 @@ export type CreateExtensionBlueprintOptions< [name in string]: AnyExtensionDataRef; }, > = { - kind: string; - namespace?: string; + kind: TKind; + namespace?: TNamespace; attachTo: { id: string; input: string; @@ -670,6 +752,7 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; + name?: TName; config?: { schema: TConfigSchema; }; @@ -742,6 +825,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 +838,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 +853,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 +877,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 +890,29 @@ export function createExternalRouteRef< ? TParams : { [param in TParamKeys]: string; - }, - TOptional + } >; -// @public +// @public (undocumented) +export function createFrontendPlugin< + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], +>( + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +>; + +// @public @deprecated export function createNavItemExtension(options: { namespace?: string; name?: string; @@ -831,7 +925,12 @@ export function createNavItemExtension(options: { }, { title?: string | undefined; - } + }, + never, + never, + string | undefined, + string | undefined, + string | undefined >; // @public (undocumented) @@ -848,13 +947,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 +976,7 @@ export namespace createNavLogoExtension { >; } -// @public +// @public @deprecated export function createPageExtension< TConfig extends { path: string; @@ -900,13 +1007,8 @@ export function createPageExtension< }, ): ExtensionDefinition; -// @public (undocumented) -export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, ->( - options: PluginOptions, -): BackstagePlugin; +// @public @deprecated (undocumented) +export const createPlugin: typeof createFrontendPlugin; // @public export function createRouteRef< @@ -928,7 +1030,7 @@ export function createRouteRef< } >; -// @public +// @public @deprecated export function createRouterExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -967,7 +1069,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 +1108,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 +1131,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 +1199,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 +1213,6 @@ export interface ExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown; }, @@ -1105,57 +1225,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?: ResolveInputValueOverrides; + }, + ) => 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 +1318,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 +1343,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 +1406,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 +1435,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?: ResolveInputValueOverrides; + }) => 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 +1554,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 +1591,9 @@ export { googleAuthApiRef }; // @public (undocumented) export const IconBundleBlueprint: ExtensionBlueprint< + 'icon-bundle', + 'app', + undefined, { icons: { [x: string]: IconComponent; @@ -1352,16 +1606,7 @@ export const IconBundleBlueprint: ExtensionBlueprint< 'core.icons', {} >, - { - [x: string]: ExtensionInput< - AnyExtensionDataRef, - { - optional: boolean; - singleton: boolean; - } - >; - }, - AnyExtensionDataRef, + {}, { icons: string; test: string; @@ -1412,9 +1657,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { - [key: string]: (zImpl: typeof z) => z.ZodType; - }, > { // (undocumented) attachTo: { @@ -1422,24 +1664,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 +1703,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 +1788,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', + { + 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) @@ -1546,6 +1875,73 @@ export type ResolvedExtensionInputs< : Expand | undefined>; }; +// @public (undocumented) +export type ResolveInputValueOverrides< + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + } = { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, +> = Expand< + { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? never + : KName + : never]: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { + optional: boolean; + singleton: infer ISingleton extends boolean; + } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } & { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? KName + : never + : never]?: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { + optional: boolean; + singleton: infer ISingleton extends boolean; + } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } +>; + // @public export type RouteFunc = ( ...[params]: TParams extends undefined @@ -1553,6 +1949,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 +1995,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } @@ -1587,6 +2012,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 +2055,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 +2137,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..65575bad9c 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.3", "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/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts similarity index 52% rename from packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts rename to packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts index 746f480c41..b5ba9e05f6 100644 --- a/packages/backend-app-api/src/services/implementations/userInfo/userInfoServiceFactory.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -14,11 +14,23 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { userInfoServiceFactory as _userInfoServiceFactory } from '../../../../../backend-defaults/src/entrypoints/userInfo'; +import { createExtensionBlueprint } from '../wiring'; +import { createApiExtension } from '../extensions/createApiExtension'; +import { AnyApiFactory } from '@backstage/core-plugin-api'; /** + * Creates utility API extensions. + * * @public - * @deprecated Please import from `@backstage/backend-defaults/userInfo` instead. */ -export const userInfoServiceFactory = _userInfoServiceFactory; +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..daa21f88bb --- /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.get(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.get(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..b976196080 --- /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.get(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..dedc5454bd --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -0,0 +1,157 @@ +/* + * 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, + renderInTestApp, +} 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.get(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 } = renderInTestApp(tester.reactElement()); + + 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..e6389da574 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -0,0 +1,73 @@ +/* + * 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, + renderInTestApp, +} from '@backstage/frontend-test-utils'; +import { screen, waitFor } from '@testing-library/react'; + +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); + + const Component = tester.get(SignInPageBlueprint.dataRefs.component); + + renderInTestApp( {}} />); + + 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..e6daf510eb --- /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).get(ThemeBlueprint.dataRefs.theme), + ).toEqual(theme); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts similarity index 51% rename from packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts rename to packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index 373dd10ead..686c498c06 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/httpAuthServiceFactory.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -14,11 +14,24 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { httpAuthServiceFactory as _httpAuthServiceFactory } from '../../../../../backend-defaults/src/entrypoints/httpAuth'; +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 - * @deprecated Please import from `@backstage/backend-defaults/httpAuth` instead. */ -export const httpAuthServiceFactory = _httpAuthServiceFactory; +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..3c7b271322 --- /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).get( + 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/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 1f12f127e8..403d591db1 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -30,24 +30,26 @@ import { } from '@backstage/core-plugin-api'; import { createRouteRef } from '../routing'; import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { createApiExtension } from '../extensions'; +import { ApiBlueprint } from '../blueprints'; const wrapInBoundaryExtension = (element?: JSX.Element) => { const routeRef = createRouteRef(); return createExtension({ name: 'test', attachTo: { id: 'app/routes', input: 'routes' }, - output: { - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + ], factory({ node }) { - return { - routeRef, - path: '/', - element: {element}, - }; + return [ + coreExtensionData.reactElement( + {element}, + ), + coreExtensionData.routePath('/'), + coreExtensionData.routeRef(routeRef), + ]; }, }); }; @@ -134,8 +136,11 @@ describe('ExtensionBoundary', () => { await act(async () => { createExtensionTester(wrapInBoundaryExtension()) .add( - createApiExtension({ - factory: createApiFactory(analyticsApiRef, analyticsApiMock), + ApiBlueprint.make({ + namespace: analyticsApiRef.id, + params: { + factory: createApiFactory(analyticsApiRef, analyticsApiMock), + }, }), ) .render(); 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..5902c5c3e7 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,550 @@ 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.get(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).get(stringDataRef)).toBe( + 'foo-boom-override-hello', + ); + + expect( + createExtensionTester(overriden, { + config: { foo: 'hello', bar: 'world' }, + }).get(stringDataRef), + ).toBe('foo-hello-override-world'); + }); + + it('should be able to override input values', () => { + const outputRef = createExtensionDataRef().with({ + id: 'output', + }); + const testDataRef1 = createExtensionDataRef().with({ + id: 'test1', + }); + const testDataRef2 = createExtensionDataRef().with({ + id: 'test2', + }); + + const subject = createExtension({ + name: 'subject', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + opt: createExtensionInput([testDataRef1.optional()], { + singleton: true, + optional: true, + }), + single: createExtensionInput( + [testDataRef1, testDataRef2.optional()], + { + singleton: true, + }, + ), + multi: createExtensionInput([testDataRef1]), + }, + output: [outputRef], + factory({ inputs }) { + return [ + outputRef({ + opt: inputs.opt?.get(testDataRef1) ?? 'none', + single: inputs.single.get(testDataRef1), + singleOpt: inputs.single.get(testDataRef2) ?? 'none', + multi: inputs.multi + .map(i => `${i.node.spec.id}=${i.get(testDataRef1)}`) + .join(','), + }), + ]; + }, + }); + + const optExt = createExtension({ + name: 'o', + attachTo: { id: 'subject', input: 'opt' }, + output: [testDataRef1], + factory: () => [testDataRef1('orig-opt')], + }); + + const singleExt = createExtension({ + name: 's', + attachTo: { id: 'subject', input: 'single' }, + output: [testDataRef1, testDataRef2.optional()], + factory: () => [testDataRef1('orig-single')], + }); + + const multi1Ext = createExtension({ + name: 'm1', + attachTo: { id: 'subject', input: 'multi' }, + output: [testDataRef1], + factory: () => [testDataRef1('orig-multi1')], + }); + + const multi2Ext = createExtension({ + name: 'm2', + attachTo: { id: 'subject', input: 'multi' }, + output: [testDataRef1], + factory: () => [testDataRef1('orig-multi2')], + }); + + expect( + createExtensionTester(subject) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'm1=orig-multi1,m2=orig-multi2', + }); + + // All values provided + expect( + createExtensionTester( + subject.override({ + factory(originalFactory) { + return originalFactory({ + inputs: { + opt: [testDataRef1('opt')], + single: [testDataRef1('single'), testDataRef2('singleOpt')], + multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'opt', + single: 'single', + singleOpt: 'singleOpt', + multi: 'm1=multi1,m2=multi2', + }); + + // Minimal values provided + expect( + createExtensionTester( + subject.override({ + factory(originalFactory) { + return originalFactory({ + inputs: { + single: [testDataRef1('single')], + multi: [], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'none', + single: 'single', + singleOpt: 'none', + multi: '', + }); + + // Forward inputs directly + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'm1=orig-multi1,m2=orig-multi2', + }); + + // Forward inputs separately + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + opt: inputs.opt, + single: inputs.single, + multi: inputs.multi, + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'm1=orig-multi1,m2=orig-multi2', + }); + + // Reordering inputs + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + opt: inputs.opt, + single: inputs.single, + multi: [inputs.multi[1], inputs.multi[0]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'm2=orig-multi2,m1=orig-multi1', + }); + + // Filter out inputs + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + opt: inputs.opt, + single: inputs.single, + multi: inputs.multi.filter(i => i.node.spec.id.endsWith('2')), + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'm2=orig-multi2', + }); + + // Overriding based on original input + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + single: [ + testDataRef1(`override-${inputs.single.get(testDataRef1)}`), + testDataRef2('new-singleOpt'), + ], + multi: inputs.multi.map(i => [ + testDataRef1(`override-${i.get(testDataRef1)}`), + ]), + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toEqual({ + opt: 'none', + single: 'override-orig-single', + singleOpt: 'new-singleOpt', + multi: 'm1=override-orig-multi1,m2=override-orig-multi2', + }); + + // Mismatched input override length + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + ...inputs, + multi: [[testDataRef1('multi1')]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, + ); + + // Mix forward and data override + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + ...inputs, + multi: [inputs.multi[0], [testDataRef1('multi2')]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`, + ); + + // Required input not provided + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + ...inputs, + single: [testDataRef2('singleOpt')], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, + ); + + // Wrong value provided + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory) { + return originalFactory({ + inputs: { + // @ts-expect-error + opt: [testDataRef2('opt')], + // @ts-expect-error + single: [testDataRef1('single'), outputRef({})], + multi: [ + // @ts-expect-error + [testDataRef2('multi1')], + ], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .get(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, + ); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 4d31982ed3..86fcdc56ee 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -17,9 +17,16 @@ import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; +import { + ResolveInputValueOverrides, + resolveInputOverrides, +} from './resolveInputOverrides'; +import { + ExtensionDataContainer, + createExtensionDataContainer, +} from './createExtensionDataContainer'; import { AnyExtensionDataRef, - ExtensionDataRef, ExtensionDataValue, } from './createExtensionDataRef'; import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; @@ -63,18 +70,6 @@ export type ExtensionDataValues = { : never]?: TExtensionData[DataName]['T']; }; -/** @public */ -export type ExtensionDataContainer = - { - get( - ref: ExtensionDataRef, - ): UExtensionData extends ExtensionDataRef - ? IConfig['optional'] extends true - ? IData | undefined - : IData - : never; - }; - /** * Convert a single extension input into a matching resolved input. * @public @@ -117,7 +112,6 @@ export interface LegacyCreateExtensionOptions< TInputs extends AnyExtensionInputMap, TConfig, TConfigInput, - TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, > { kind?: string; namespace?: string; @@ -126,25 +120,37 @@ 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>; } +type ToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never; + +type PopUnion = ToIntersection< + U extends any ? () => U : never +> extends () => infer R + ? [rest: Exclude, next: R] + : undefined; + +/** @ignore */ +type JoinStringUnion< + U, + TDiv extends string = ', ', + TResult extends string = '', +> = PopUnion extends [infer IRest extends string, infer INext extends string] + ? TResult extends '' + ? JoinStringUnion + : JoinStringUnion + : TResult; + /** @ignore */ export type VerifyExtensionFactoryOutput< UDeclaredOutput extends AnyExtensionDataRef, @@ -157,17 +163,21 @@ export type VerifyExtensionFactoryOutput< : never ) extends infer IRequiredOutputIds ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? {} - : { - 'Error: The extension factory is missing the following outputs': Exclude< - IRequiredOutputIds, - UFactoryOutput['id'] - >; - } + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} + : `Error: The extension factory has undeclared output(s): ${JoinStringUnion< + Exclude + >}` + : `Error: The extension factory is missing the following output(s): ${JoinStringUnion< + Exclude + >}` : never; /** @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 +185,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?: ResolveInputValueOverrides; + }) => 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 +385,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 +424,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 +451,11 @@ export function createExtension< >( options: | CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, - TConfig, - TConfigInput, TConfigSchema, UFactoryOutput > @@ -374,8 +463,7 @@ export function createExtension< AnyExtensionDataMap, TLegacyInputs, TConfig, - TConfigInput, - TConfigSchema + TConfigInput >, ): ExtensionDefinition< TConfig & @@ -391,21 +479,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 +532,152 @@ 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?: ResolveInputValueOverrides; + }) => 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?: ResolveInputValueOverrides; + }): ExtensionDataContainer => { + return createExtensionDataContainer( + newOptions.factory({ + node, + config: innerContext?.config ?? config, + inputs: resolveInputOverrides( + newOptions.inputs, + inputs, + innerContext?.inputs, + ) as any, // TODO: Might be able to improve this once legacy inputs are gone + }) as Iterable, + newOptions.output, + ); + }, + { + 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 +694,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..f2f17079ca 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -18,12 +18,31 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; import { createExtensionBlueprint } from './createExtensionBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { createExtensionDataRef } from './createExtensionDataRef'; +import { + ExtensionDataValue, + createExtensionDataRef, +} from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; +import { + ExtensionDefinition, + toInternalExtensionDefinition, +} from './createExtension'; +import { createExtensionDataContainer } from './createExtensionDataContainer'; function unused(..._any: any[]) {} +function factoryOutput( + ext: ExtensionDefinition, + inputs: unknown = undefined, +) { + const int = toInternalExtensionDefinition(ext); + if (int.version !== 'v2') { + throw new Error('Expected v2 extension'); + } + return Array.from(int.factory({ inputs } as any)); +} + describe('createExtensionBlueprint', () => { it('should allow creation of extension blueprints', () => { const TestExtensionBlueprint = createExtensionBlueprint({ @@ -57,6 +76,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 +134,7 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', factory(origFactory) { return origFactory({ @@ -132,11 +192,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 +240,7 @@ describe('createExtensionBlueprint', () => { }, }); - TestExtensionBlueprint.make({ + TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', params: { text: 'Hello, world!', @@ -213,11 +270,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 +341,491 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should be able to override inputs when calling original factory', () => { + const outputRef = createExtensionDataRef().with({ id: 'output' }); + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const Blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + opt: createExtensionInput([testDataRef1.optional()], { + singleton: true, + optional: true, + }), + single: createExtensionInput([testDataRef1, testDataRef2.optional()], { + singleton: true, + }), + multi: createExtensionInput([testDataRef1]), + }, + output: [outputRef], + factory(_, { inputs }) { + return [ + outputRef({ + opt: inputs.opt?.get(testDataRef1) ?? 'none', + single: inputs.single.get(testDataRef1), + singleOpt: inputs.single.get(testDataRef2) ?? 'none', + multi: inputs.multi.map(i => i.get(testDataRef1)).join(','), + }), + ]; + }, + }); + + const mockInput = (node: string, ...data: ExtensionDataValue[]) => + Object.assign(createExtensionDataContainer(data), { + node, + }); + const mockParentInputs = { + opt: mockInput('node-opt', testDataRef1('orig-opt')), + single: mockInput('node-single', testDataRef1('orig-single')), + multi: [ + mockInput('node-multi1', testDataRef1('orig-multi1')), + mockInput('node-multi2', testDataRef1('orig-multi2')), + ], + }; + + // All values provided + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + opt: [testDataRef1('opt')], + single: [testDataRef1('single'), testDataRef2('singleOpt')], + multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'opt', + single: 'single', + singleOpt: 'singleOpt', + multi: 'multi1,multi2', + }), + ]); + + // Minimal values provided + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + single: [testDataRef1('single')], + multi: [], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'none', + single: 'single', + singleOpt: 'none', + multi: '', + }), + ]); + + // Mismatched input override length + expect(() => + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + ...inputs, + multi: [[testDataRef1('multi1')]], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"override data provided for input 'multi' must match the length of the original inputs"`, + ); + + // Required input not provided + expect(() => + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + ...inputs, + single: [testDataRef2('singleOpt')], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"missing required extension data value(s) 'test1'"`, + ); + + // Wrong value provided + expect(() => + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + // @ts-expect-error + opt: [testDataRef2('opt')], + // @ts-expect-error + single: [testDataRef1('single'), outputRef({})], + multi: [ + // @ts-expect-error + [testDataRef2('multi1')], + ], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"extension data 'test2' was provided but not declared"`, + ); + + // Forwarding entire inputs object + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory({}, { inputs }); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }), + ]); + + // Forwarding individual outputs + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + opt: inputs.opt, + single: inputs.single, + multi: inputs.multi, + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }), + ]); + + // Overriding based on original input + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + single: [ + testDataRef1(`override-${inputs.single.get(testDataRef1)}`), + testDataRef2('new-singleOpt'), + ], + multi: inputs.multi.map(i => [ + testDataRef1(`override-${i.get(testDataRef1)}`), + ]), + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'none', + single: 'override-orig-single', + singleOpt: 'new-singleOpt', + multi: 'override-orig-multi1,override-orig-multi2', + }), + ]); + }); + + 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 reject invalid output from original factory', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + expect(() => + factoryOutput( + // @ts-expect-error + createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef2('foo')]; + }, + }).makeWithOverrides({ factory: orig => orig({}) }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"extension data 'test2' was provided but not declared"`, + ); + + expect(() => + factoryOutput( + // @ts-expect-error + createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return []; + }, + }).makeWithOverrides({ factory: orig => orig({}) }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"missing required extension data value(s) 'test1'"`, + ); + }); + + 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..d1fc75bca6 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -27,14 +27,24 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, - ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; +import { + ExtensionDataContainer, + createExtensionDataContainer, +} from './createExtensionDataContainer'; +import { + ResolveInputValueOverrides, + resolveInputOverrides, +} from './resolveInputOverrides'; /** * @public */ export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -47,12 +57,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 +85,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 +96,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?: ResolveInputValueOverrides; + }, + ) => 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,7 +192,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 >; } @@ -163,6 +205,9 @@ export interface ExtensionBlueprint< * @internal */ class ExtensionBlueprintImpl< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -171,12 +216,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 +237,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?: { @@ -215,9 +270,9 @@ class ExtensionBlueprintImpl< ReturnType >; }; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: { @@ -227,7 +282,7 @@ class ExtensionBlueprintImpl< } & { [key in keyof TConfigSchema]: z.infer>; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; }): ExtensionDefinition< @@ -254,74 +309,82 @@ 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?: ResolveInputValueOverrides; + }, + ): ExtensionDataContainer => { + return createExtensionDataContainer( + 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: resolveInputOverrides( + this.options.inputs, + inputs, + innerContext?.inputs, + ) as any, // TODO: Might be able to improve this once legacy inputs are gone + }), + this.options.output, + ); + }, + { 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 +403,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 +422,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 +441,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/createExtensionDataContainer.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataContainer.ts new file mode 100644 index 0000000000..d79ec70f2f --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataContainer.ts @@ -0,0 +1,88 @@ +/* + * 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 { + AnyExtensionDataRef, + ExtensionDataRef, + ExtensionDataValue, +} from './createExtensionDataRef'; + +/** @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 + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; + }; + +/** @internal */ +export function createExtensionDataContainer( + values: Iterable< + UData extends ExtensionDataRef + ? ExtensionDataValue + : never + >, + declaredRefs?: ExtensionDataRef[], +): ExtensionDataContainer { + const container = new Map>(); + const verifyRefs = + declaredRefs && new Map(declaredRefs.map(ref => [ref.id, ref])); + + for (const output of values) { + if (verifyRefs) { + if (!verifyRefs.delete(output.id)) { + throw new Error( + `extension data '${output.id}' was provided but not declared`, + ); + } + } + container.set(output.id, output); + } + + const remainingRefs = + verifyRefs && + Array.from(verifyRefs.values()).filter(ref => !ref.config.optional); + if (remainingRefs && remainingRefs.length > 0) { + throw new Error( + `missing required extension data value(s) '${remainingRefs + .map(ref => ref.id) + .join(', ')}'`, + ); + } + + return { + get(ref) { + return container.get(ref.id)?.value; + }, + [Symbol.iterator]() { + return container.values(); + }, + } as ExtensionDataContainer; +} diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index c77aa8c394..80413c1688 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -55,7 +55,7 @@ export interface ConfigurableExtensionDataRef< optional(): ConfigurableExtensionDataRef< TData, TId, - TData & { optional: true } + TConfig & { optional: true } >; (t: TData): ExtensionDataValue; } diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts similarity index 67% rename from packages/frontend-plugin-api/src/wiring/createPlugin.test.ts rename to packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts index bb9bdc2107..71508b0c15 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts @@ -18,7 +18,7 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { screen } from '@testing-library/react'; import { createSchemaFromZod } from '../schema/createSchemaFromZod'; -import { createPlugin } from './createPlugin'; +import { createFrontendPlugin } from './createFrontendPlugin'; import { JsonObject } from '@backstage/types'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; @@ -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')]; }, }); @@ -135,21 +133,46 @@ function createTestAppRoot({ }).createRoot(); } -describe('createPlugin', () => { +describe('createFrontendPlugin', () => { it('should create an empty plugin', () => { - const plugin = createPlugin({ id: 'test' }); + const plugin = createFrontendPlugin({ id: 'test' }); expect(plugin).toBeDefined(); expect(String(plugin)).toBe('Plugin{id=test}'); }); it('should create a plugin with extension instances', async () => { - const plugin = createPlugin({ + const plugin = createFrontendPlugin({ id: 'test', extensions: [Extension1, Extension2, outputExtension], }); expect(plugin).toBeDefined(); + expect(plugin.getExtension('test/1')).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "test/output", + "input": "names", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": undefined, + "name": "1", + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + // @ts-expect-error + expect(plugin.getExtension('nonexistent')).toBeUndefined(); + await renderWithEffects( createTestAppRoot({ features: [plugin], @@ -163,7 +186,7 @@ describe('createPlugin', () => { }); it('should create a plugin with nested extension instances', async () => { - const plugin = createPlugin({ + const plugin = createFrontendPlugin({ id: 'test', extensions: [Extension1, Extension2, Extension3, Child, outputExtension], }); @@ -195,7 +218,7 @@ describe('createPlugin', () => { }); it('should create a plugin with nested extension instances and multiple children', async () => { - const plugin = createPlugin({ + const plugin = createFrontendPlugin({ id: 'test', extensions: [ Extension1, @@ -228,14 +251,14 @@ describe('createPlugin', () => { it('should throw on duplicate extensions', async () => { expect(() => - createPlugin({ + createFrontendPlugin({ id: 'test', extensions: [Extension1, Extension1], }), ).toThrow("Plugin 'test' provided duplicate extensions: test/1"); expect(() => - createPlugin({ + createFrontendPlugin({ id: 'test', extensions: [ Extension1, @@ -248,4 +271,68 @@ describe('createPlugin', () => { }), ).toThrow("Plugin 'test' provided duplicate extensions: test/2, test/3"); }); + + describe('overrides', () => { + it('should return a plugin instance with the correct namespace', () => { + const plugin = createFrontendPlugin({ + id: 'test', + extensions: [Extension1, Extension2], + }); + + expect(plugin.getExtension('test/1')).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "test/output", + "input": "names", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": undefined, + "name": "1", + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should allow overriding extensions that have a matching ID, while keeping old extensions that do not have overlapping IDs', async () => { + const plugin = createFrontendPlugin({ + id: 'test', + extensions: [Extension1, Extension2, outputExtension], + }); + + await renderWithEffects( + createTestAppRoot({ + features: [ + plugin.withOverrides({ + extensions: [ + plugin.getExtension('test/1').override({ + factory() { + return [nameExtensionDataRef('overridden')]; + }, + }), + ], + }), + ], + config: { + app: { + extensions: [{ 'app/root': false }], + }, + }, + }), + ); + + await expect( + screen.findByText('Names: extension-2, overridden'), + ).resolves.toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts new file mode 100644 index 0000000000..bab09c631e --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts @@ -0,0 +1,153 @@ +/* + * 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 { ExtensionDefinition } from './createExtension'; +import { + Extension, + ResolveExtensionId, + resolveExtensionDefinition, +} from './resolveExtensionDefinition'; +import { + AnyExternalRoutes, + AnyRoutes, + BackstagePlugin, + FeatureFlagConfig, +} from './types'; + +/** @public */ +export interface PluginOptions< + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], +> { + id: TId; + routes?: TRoutes; + externalRoutes?: TExternalRoutes; + extensions?: TExtensions; + featureFlags?: FeatureFlagConfig[]; +} + +/** @public */ +export interface InternalBackstagePlugin< + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, +> extends BackstagePlugin { + readonly version: 'v1'; + readonly extensions: Extension[]; + readonly featureFlags: FeatureFlagConfig[]; +} + +/** @public */ +export function createFrontendPlugin< + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], +>( + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +> { + const extensions = new Array>(); + const extensionDefinitionsById = new Map< + string, + ExtensionDefinition + >(); + + for (const def of options.extensions ?? []) { + const ext = resolveExtensionDefinition(def, { namespace: options.id }); + extensions.push(ext); + extensionDefinitionsById.set(ext.id, { ...def, namespace: options.id }); + } + + 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), + ), + ); + // TODO(Rugvip): This could provide some more information about the kind + name of the extensions + throw new Error( + `Plugin '${options.id}' provided duplicate extensions: ${duplicates.join( + ', ', + )}`, + ); + } + + return { + $$type: '@backstage/BackstagePlugin', + version: 'v1', + id: options.id, + 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}}`; + }, + withOverrides(overrides) { + const overriddenExtensionIds = new Set( + overrides.extensions.map( + e => resolveExtensionDefinition(e, { namespace: options.id }).id, + ), + ); + const nonOverriddenExtensions = (options.extensions ?? []).filter( + e => + !overriddenExtensionIds.has( + resolveExtensionDefinition(e, { namespace: options.id }).id, + ), + ); + return createFrontendPlugin({ + ...options, + extensions: [...nonOverriddenExtensions, ...overrides.extensions], + }); + }, + } as InternalBackstagePlugin; +} + +/** @internal */ +export function toInternalBackstagePlugin( + plugin: BackstagePlugin, +): InternalBackstagePlugin { + const internal = plugin as InternalBackstagePlugin; + if (internal.$$type !== '@backstage/BackstagePlugin') { + throw new Error(`Invalid plugin instance, bad type '${internal.$$type}'`); + } + if (internal.version !== 'v1') { + throw new Error( + `Invalid plugin instance, bad version '${internal.version}'`, + ); + } + return internal; +} + +/** + * @public + * @deprecated Use {@link createFrontendPlugin} instead. + */ +export const createPlugin = createFrontendPlugin; diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts deleted file mode 100644 index b671d95d2f..0000000000 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { ExtensionDefinition } from './createExtension'; -import { - Extension, - resolveExtensionDefinition, -} from './resolveExtensionDefinition'; -import { - AnyExternalRoutes, - AnyRoutes, - BackstagePlugin, - FeatureFlagConfig, -} from './types'; - -/** @public */ -export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, -> { - id: string; - routes?: Routes; - externalRoutes?: ExternalRoutes; - extensions?: ExtensionDefinition[]; - featureFlags?: FeatureFlagConfig[]; -} - -/** @public */ -export interface InternalBackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, -> extends BackstagePlugin { - readonly version: 'v1'; - readonly extensions: Extension[]; - readonly featureFlags: FeatureFlagConfig[]; -} - -/** @public */ -export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, ->( - options: PluginOptions, -): BackstagePlugin { - const extensions = (options.extensions ?? []).map(def => - resolveExtensionDefinition(def, { namespace: options.id }), - ); - - const extensionIds = extensions.map(e => e.id); - if (extensionIds.length !== new Set(extensionIds).size) { - const duplicates = Array.from( - new Set( - extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index), - ), - ); - // TODO(Rugvip): This could provide some more information about the kind + name of the extensions - throw new Error( - `Plugin '${options.id}' provided duplicate extensions: ${duplicates.join( - ', ', - )}`, - ); - } - - return { - $$type: '@backstage/BackstagePlugin', - version: 'v1', - id: options.id, - routes: options.routes ?? ({} as Routes), - externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes), - featureFlags: options.featureFlags ?? [], - extensions, - toString() { - return `Plugin{id=${options.id}}`; - }, - } as InternalBackstagePlugin; -} - -/** @internal */ -export function toInternalBackstagePlugin( - plugin: BackstagePlugin, -): InternalBackstagePlugin { - const internal = plugin as InternalBackstagePlugin; - if (internal.$$type !== '@backstage/BackstagePlugin') { - throw new Error(`Invalid plugin instance, bad type '${internal.$$type}'`); - } - if (internal.version !== 'v1') { - throw new Error( - `Invalid plugin instance, bad version '${internal.version}'`, - ); - } - return internal; -} diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index a17dfedc16..0b64490742 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -17,7 +17,6 @@ export { coreExtensionData } from './coreExtensionData'; export { createExtension, - type ExtensionDataContainer, type ExtensionDefinition, type CreateExtensionOptions, type ExtensionDataValues, @@ -32,6 +31,7 @@ export { type ExtensionInput, type LegacyExtensionInput, } from './createExtensionInput'; +export { type ExtensionDataContainer } from './createExtensionDataContainer'; export { createExtensionDataRef, type AnyExtensionDataRef, @@ -40,7 +40,11 @@ export { type ExtensionDataValue, type ConfigurableExtensionDataRef, } from './createExtensionDataRef'; -export { createPlugin, type PluginOptions } from './createPlugin'; +export { + createPlugin, + createFrontendPlugin, + type PluginOptions, +} from './createFrontendPlugin'; export { createExtensionOverrides, type ExtensionOverridesOptions, @@ -59,3 +63,4 @@ export { type ExtensionBlueprint, createExtensionBlueprint, } from './createExtensionBlueprint'; +export { type ResolveInputValueOverrides } from './resolveInputOverrides'; 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/resolveInputOverrides.ts b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts new file mode 100644 index 0000000000..cf201779e0 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts @@ -0,0 +1,179 @@ +/* + * 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 { AppNode } from '../apis'; +import { Expand } from '../types'; +import { ResolvedExtensionInput } from './createExtension'; +import { + ExtensionDataContainer, + createExtensionDataContainer, +} from './createExtensionDataContainer'; +import { + AnyExtensionDataRef, + ExtensionDataRefToValue, + ExtensionDataValue, +} from './createExtensionDataRef'; +import { ExtensionInput } from './createExtensionInput'; + +/** @public */ +export type ResolveInputValueOverrides< + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, +> = Expand< + { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? never + : KName + : never]: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { optional: boolean; singleton: infer ISingleton extends boolean } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } & { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? KName + : never + : never]?: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { optional: boolean; singleton: infer ISingleton extends boolean } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } +>; + +function expectArray(value: T | T[]): T[] { + return value as T[]; +} +function expectItem(value: T | T[]): T { + return value as T; +} + +/** @internal */ +export function resolveInputOverrides( + declaredInputs?: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + inputs?: { + [KName in string]?: + | ({ node: AppNode } & ExtensionDataContainer) + | Array<{ node: AppNode } & ExtensionDataContainer>; + }, + inputOverrides?: ResolveInputValueOverrides, +) { + if (!declaredInputs || !inputs || !inputOverrides) { + return inputs; + } + + const newInputs: typeof inputs = {}; + for (const name in declaredInputs) { + if (!Object.hasOwn(declaredInputs, name)) { + continue; + } + const declaredInput = declaredInputs[name]; + const providedData = inputOverrides[name]; + if (declaredInput.config.singleton) { + const originalInput = expectItem(inputs[name]); + if (providedData) { + const providedContainer = createExtensionDataContainer( + providedData as Iterable>, + declaredInput.extensionData, + ); + if (!originalInput) { + throw new Error( + `attempted to override data of input '${name}' but it is not present in the original inputs`, + ); + } + newInputs[name] = Object.assign(providedContainer, { + node: (originalInput as ResolvedExtensionInput).node, + }) as any; + } + } else { + const originalInput = expectArray(inputs[name]); + if (!Array.isArray(providedData)) { + throw new Error( + `override data provided for input '${name}' must be an array`, + ); + } + + // Regular inputs can be overridden in two different ways: + // 1) Forward a subset of the original inputs in a new order + // 2) Provide new data for each original input + + // First check if all inputs are being removed + if (providedData.length === 0) { + newInputs[name] = []; + } else { + // Check how many of the provided data items have a node property, i.e. is a forwarded input + const withNodesCount = providedData.filter(d => 'node' in d).length; + if (withNodesCount === 0) { + if (originalInput.length !== providedData.length) { + throw new Error( + `override data provided for input '${name}' must match the length of the original inputs`, + ); + } + newInputs[name] = providedData.map((data, i) => { + const providedContainer = createExtensionDataContainer( + data as Iterable>, + declaredInput.extensionData, + ); + return Object.assign(providedContainer, { + node: (originalInput[i] as ResolvedExtensionInput).node, + }) as any; + }); + } else if (withNodesCount === providedData.length) { + newInputs[name] = providedData as any; + } else { + throw new Error( + `override data for input '${name}' may not mix forwarded inputs with data overrides`, + ); + } + } + } + } + return newInputs; +} diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index 7e0650e06c..4a7d972b37 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,27 @@ 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]; + withOverrides(options: { + extensions: Array>; + }): BackstagePlugin; } /** @public */ diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index 8f770b7b31..5c8626f7a7 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/frontend-test-utils +## 0.1.12-next.3 + +### Patch Changes + +- 2d21599: 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)} + + ); + } + }); + + ``` + +- 264e10f: Deprecate existing `ExtensionCreators` in favour of their new Blueprint counterparts. +- 264e10f: 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. + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.3 + - @backstage/config@1.2.0 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/types@1.1.1 + +## 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 3aa9069219..164c222d84 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -7,6 +7,7 @@ import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; +import { AnyExtensionDataRef } 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'; @@ -30,20 +31,30 @@ import { TestApiRegistry } from '@backstage/test-utils'; import { withLogCollector } from '@backstage/test-utils'; // @public (undocumented) -export function createExtensionTester( - subject: ExtensionDefinition, +export function createExtensionTester< + TConfig, + TConfigInput, + UOutput extends AnyExtensionDataRef, +>( + subject: ExtensionDefinition, options?: { - config?: TConfig; + config?: TConfigInput; }, -): ExtensionTester; +): ExtensionTester; export { ErrorWithContext }; // @public (undocumented) -export class ExtensionQuery { +export class ExtensionQuery { constructor(node: AppNode); // (undocumented) - data(ref: ExtensionDataRef): T | undefined; + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; // (undocumented) get instance(): AppNodeInstance; // (undocumented) @@ -51,19 +62,29 @@ export class ExtensionQuery { } // @public (undocumented) -export class ExtensionTester { +export class ExtensionTester { // (undocumented) add( extension: ExtensionDefinition, options?: { config?: TConfigInput; }, - ): ExtensionTester; + ): ExtensionTester; // (undocumented) - data(ref: ExtensionDataRef): T | undefined; + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; // (undocumented) - query(id: string | ExtensionDefinition): ExtensionQuery; + query( + extension: ExtensionDefinition, + ): ExtensionQuery; // (undocumented) + reactElement(): JSX.Element; + // @deprecated (undocumented) render(options?: { config?: JsonObject }): RenderResult; } @@ -117,6 +138,7 @@ export type TestAppOptions = { mountedRoutes?: { [path: string]: RouteRef; }; + config?: JsonObject; }; export { withLogCollector }; diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 737d30363a..80e8a1c4ac 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.3", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 889c815fc7..14ffd05ada 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -26,7 +26,6 @@ import { createExtension, createExtensionDataRef, createExtensionInput, - createSchemaFromZod, useAnalytics, useApi, } from '@backstage/frontend-plugin-api'; @@ -41,8 +40,8 @@ describe('createExtensionTester', () => { const defaultDefinition = { namespace: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: { element: coreExtensionData.reactElement }, - factory: () => ({ element:
test
}), + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
test
)], }; it('should render a simple extension', async () => { @@ -77,23 +76,23 @@ describe('createExtensionTester', () => { it('should render multiple extensions', async () => { const indexPageExtension = createExtension({ ...defaultDefinition, - factory: () => ({ - element: ( + factory: () => [ + coreExtensionData.reactElement(
Index page See details -
+
, ), - }), + ], }); const detailsPageExtension = createExtension({ ...defaultDefinition, name: 'details', attachTo: { id: 'app/routes', input: 'routes' }, - output: { - path: coreExtensionData.routePath, - element: coreExtensionData.reactElement, - }, - factory: () => ({ path: '/details', element:
Details page
}), + output: [coreExtensionData.routePath, coreExtensionData.reactElement], + factory: () => [ + coreExtensionData.routePath('/details'), + coreExtensionData.reactElement(
Details page
), + ], }); const tester = createExtensionTester(indexPageExtension); @@ -112,9 +111,11 @@ describe('createExtensionTester', () => { it('should accepts a custom config', async () => { const indexPageExtension = createExtension({ ...defaultDefinition, - configSchema: createSchemaFromZod(z => - z.object({ title: z.string().optional() }), - ), + config: { + schema: { + title: z => z.string().optional(), + }, + }, factory: ({ config }) => { const Component = () => { const configApi = useApi(configApiRef); @@ -127,9 +128,8 @@ describe('createExtensionTester', () => {
); }; - return { - element: , - }; + + return [coreExtensionData.reactElement()]; }, }); @@ -137,17 +137,18 @@ describe('createExtensionTester', () => { ...defaultDefinition, name: 'details', attachTo: { id: 'app/routes', input: 'routes' }, - configSchema: createSchemaFromZod(z => - z.object({ title: z.string().optional() }), - ), - output: { - path: coreExtensionData.routePath, - element: coreExtensionData.reactElement, + config: { + schema: { + title: z => z.string().optional(), + }, }, - factory: ({ config }) => ({ - path: '/details', - element:
{config.title ?? 'Details page'}
, - }), + output: [coreExtensionData.routePath, coreExtensionData.reactElement], + factory: ({ config }) => [ + coreExtensionData.routePath('/details'), + coreExtensionData.reactElement( +
{config.title ?? 'Details page'}
, + ), + ], }); const tester = createExtensionTester(indexPageExtension, { @@ -209,9 +210,7 @@ describe('createExtensionTester', () => { ); }; - return { - element: , - }; + return [coreExtensionData.reactElement()]; }, }); @@ -240,13 +239,13 @@ describe('createExtensionTester', () => { const extension = createExtension({ namespace: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('test-text')], }); const tester = createExtensionTester(extension); - expect(tester.data(stringDataRef)).toBe('test-text'); + expect(tester.get(stringDataRef)).toBe('test-text'); }); it('should throw an error if trying to access an instance not provided to the tester', () => { @@ -254,16 +253,16 @@ describe('createExtensionTester', () => { namespace: 'test', name: 'e1', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('test-text')], }); const extension2 = createExtension({ namespace: 'test', name: 'e2', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('test-text')], }); const tester = createExtensionTester(extension); @@ -278,16 +277,16 @@ describe('createExtensionTester', () => { namespace: 'test', name: 'e1', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('test-text')], }); const extension2 = createExtension({ namespace: 'test', name: 'e2', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('test-text')], }); const tester = createExtensionTester(extension).add(extension2); @@ -297,6 +296,79 @@ describe('createExtensionTester', () => { ); }); + it('should not allow getting extension data for an output that was not defined in the extension', () => { + const internalRef = createExtensionDataRef().with({ + id: 'test.internal', + }); + + const internalRef2 = createExtensionDataRef().with({ + id: 'test.internal2', + }); + + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [stringDataRef, internalRef.optional()], + factory: () => [stringDataRef('test-text')], + }); + + const tester = createExtensionTester(extension); + + const test: string = tester.get(stringDataRef); + + // @ts-expect-error - internalRef is optional + const test2: number = tester.get(internalRef); + + // @ts-expect-error - internalRef2 is not defined in the extension + const test3: number = tester.get(internalRef2); + + expect([test, test2, test3]).toBeDefined(); + }); + + it('should support getting outputs from a query response', () => { + const internalRef = createExtensionDataRef().with({ + id: 'test.internal', + }); + + const internalRef2 = createExtensionDataRef().with({ + id: 'test.internal2', + }); + + const extension = createExtension({ + namespace: 'test', + name: 'e1', + inputs: { + ignored: createExtensionInput([stringDataRef]), + }, + attachTo: { id: 'ignored', input: 'ignored' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
bob
)], + }); + + const extraExtension = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'test/e1', input: 'ignored' }, + output: [stringDataRef, internalRef.optional()], + factory: () => [stringDataRef('test-text')], + }); + + const tester = createExtensionTester(extension) + .add(extraExtension) + .query(extraExtension); + + const test: string = tester.get(stringDataRef); + + // @ts-expect-error - internalRef is optional + const test2: number = tester.get(internalRef); + + // @ts-expect-error - internalRef2 is not defined in the extension + const test3: number = tester.get(internalRef2); + + expect([test, test2, test3]).toBeDefined(); + }); + // TODO: this should be implemented // eslint-disable-next-line jest/no-disabled-tests it.skip('should allow querying an extension and getting outputs', () => { @@ -304,32 +376,27 @@ describe('createExtensionTester', () => { namespace: 'test', name: 'e1', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, + output: [stringDataRef], inputs: { - input: createExtensionInput( - { - output: stringDataRef, - }, - { singleton: true }, - ), + input: createExtensionInput([stringDataRef], { singleton: true }), }, - factory: ({ inputs }) => ({ - text: `nest-${inputs.input.output.output}`, - }), + factory: ({ inputs }) => [ + stringDataRef(`nest-${inputs.input.get(stringDataRef)}`), + ], }); const extension2 = createExtension({ namespace: 'test', name: 'e2', attachTo: { id: 'test/e1', input: 'blob' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('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'); + expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension2).get(stringDataRef)).toBe('test-text'); // @ts-expect-error expect(tester.query(extension).input('input').data(stringDataRef)).toBe( 'nest-test-text', @@ -343,18 +410,13 @@ describe('createExtensionTester', () => { namespace: 'test', name: 'e1', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, + output: [stringDataRef], inputs: { - input: createExtensionInput( - { - output: stringDataRef, - }, - { singleton: true }, - ), + input: createExtensionInput([stringDataRef], { singleton: true }), }, - factory: ({ inputs }) => ({ - text: `nest-${inputs.input.output.output}`, - }), + factory: ({ inputs }) => [ + stringDataRef(`nest-${inputs.input.get(stringDataRef)}`), + ], }); const tester = createExtensionTester(extension, { @@ -362,6 +424,6 @@ describe('createExtensionTester', () => { inputs: { input: 'test-text' }, }); - expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension).get(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 1d41958a09..b3c1b385e3 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -19,7 +19,7 @@ import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { - ExtensionDataValue, + AnyExtensionDataRef, AppNode, AppTree, Extension, @@ -56,10 +56,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}
  • @@ -99,7 +102,7 @@ const TestAppNavExtension = createExtension({ }); /** @public */ -export class ExtensionQuery { +export class ExtensionQuery { #node: AppNode; constructor(node: AppNode) { @@ -122,18 +125,24 @@ export class ExtensionQuery { return instance; } - data(ref: ExtensionDataRef): T | undefined { + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never { return this.instance.getData(ref); } } /** @public */ -export class ExtensionTester { +export class ExtensionTester { /** @internal */ - static forSubject( + static forSubject( subject: ExtensionDefinition, options?: { config?: TConfigInput }, - ): ExtensionTester { + ): ExtensionTester { const tester = new ExtensionTester(); const internal = toInternalExtensionDefinition(subject); @@ -166,9 +175,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('/')]; @@ -192,7 +199,7 @@ export class ExtensionTester { add( extension: ExtensionDefinition, options?: { config?: TConfigInput }, - ): ExtensionTester { + ): ExtensionTester { if (this.#tree) { throw new Error( 'Cannot add more extensions accessing the extension tree', @@ -219,17 +226,24 @@ export class ExtensionTester { return this; } - data(ref: ExtensionDataRef): T | undefined { + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never { const tree = this.#resolveTree(); - return new ExtensionQuery(tree.root).data(ref); + return new ExtensionQuery(tree.root).get(ref); } - query(id: string | ExtensionDefinition): ExtensionQuery { + query( + extension: ExtensionDefinition, + ): ExtensionQuery { const tree = this.#resolveTree(); - const actualId = - typeof id === 'string' ? id : resolveExtensionDefinition(id).id; + const actualId = resolveExtensionDefinition(extension).id; const node = tree.nodes.get(actualId); @@ -245,6 +259,25 @@ export class ExtensionTester { return new ExtensionQuery(node); } + reactElement(): JSX.Element { + const tree = this.#resolveTree(); + + const element = new ExtensionQuery(tree.root).get( + coreExtensionData.reactElement, + ); + + if (!element) { + throw new Error( + 'No element found. Make sure the extension has a `coreExtensionData.reactElement` output, or use the `.get(...)` to access output data directly instead', + ); + } + + return element; + } + + /** + * @deprecated Switch to using `renderInTestApp` directly and using `.reactElement()` or `.get(...)` to get the component you w + */ render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; @@ -336,9 +369,13 @@ export class ExtensionTester { } /** @public */ -export function createExtensionTester( - subject: ExtensionDefinition, - options?: { config?: TConfig }, -): ExtensionTester { +export function createExtensionTester< + TConfig, + TConfigInput, + UOutput extends AnyExtensionDataRef, +>( + subject: ExtensionDefinition, + options?: { config?: TConfigInput }, +): ExtensionTester { return ExtensionTester.forSubject(subject, options); } diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index e0483f68d8..b37a7c2a8d 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -15,12 +15,23 @@ */ import React from 'react'; +import { Link, MemoryRouter } from 'react-router-dom'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { RenderResult, render } from '@testing-library/react'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; import { - RouteRef, - coreExtensionData, createExtension, + createExtensionOverrides, + ExtensionDefinition, + coreExtensionData, + RouteRef, + useRouteRef, + createExtensionInput, + IconComponent, + createNavItemExtension, + RouterBlueprint, } from '@backstage/frontend-plugin-api'; -import { createExtensionTester } from './createExtensionTester'; /** * Options to customize the behavior of the test app. @@ -44,8 +55,66 @@ export type TestAppOptions = { * ``` */ mountedRoutes?: { [path: string]: RouteRef }; + + /** + * Additional configuration passed to the app when rendering elements inside it. + */ + config?: JsonObject; }; +const NavItem = (props: { + routeRef: RouteRef; + title: string; + icon: IconComponent; +}) => { + const { routeRef, title, icon: Icon } = props; + const link = useRouteRef(routeRef); + if (!link) { + return null; + } + return ( +
  • + + {title} + +
  • + ); +}; + +export const TestAppNavExtension = createExtension({ + namespace: 'app', + name: 'nav', + attachTo: { id: 'app/layout', input: 'nav' }, + inputs: { + items: createExtensionInput([createNavItemExtension.targetDataRef]), + }, + output: [coreExtensionData.reactElement], + factory({ inputs }) { + return [ + coreExtensionData.reactElement( + , + ), + ]; + }, +}); + /** * @public * Renders the given element in a test app, for use in unit tests. @@ -53,36 +122,61 @@ export type TestAppOptions = { export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, -) { - const extension = createExtension({ - namespace: 'test', - attachTo: { id: 'app', input: 'root' }, - output: { - element: coreExtensionData.reactElement, - }, - factory: () => ({ element }), - }); - const tester = createExtensionTester(extension); +): RenderResult { + const extensions: Array> = [ + createExtension({ + namespace: 'test', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [coreExtensionData.reactElement, coreExtensionData.routePath], + factory: () => { + return [ + coreExtensionData.reactElement(element), + coreExtensionData.routePath('/'), + ]; + }, + }), + RouterBlueprint.make({ + namespace: 'test', + params: { + Component: ({ children }) => {children}, + }, + }), + TestAppNavExtension, + ]; if (options?.mountedRoutes) { for (const [path, routeRef] of Object.entries(options.mountedRoutes)) { // TODO(Rugvip): add support for external route refs - tester.add( + extensions.push( createExtension({ kind: 'test-route', name: path, attachTo: { id: 'app/root', input: 'elements' }, - output: { - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef, - }, - factory() { - return { element: , path, routeRef }; - }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + coreExtensionData.routeRef, + ], + factory: () => [ + coreExtensionData.reactElement(), + coreExtensionData.routePath(path), + coreExtensionData.routeRef(routeRef), + ], }), ); } } - return tester.render(); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions, + }), + ], + config: ConfigReader.fromConfigs([ + { context: 'render-config', data: options?.config ?? {} }, + ]), + }); + + return render(app.createRoot()); } diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 35c7f35836..f429daeb02 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/repo-tools +## 0.9.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + +## 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..c18d2c62d9 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.3", "publishConfig": { "access": "public" }, diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 728145048a..ec4a5cd46f 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,43 @@ # techdocs-cli-embedded-app +## 0.2.99-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.4 + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/app-defaults@1.5.10-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/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## 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..ece31d31f3 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.4", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index d6d5128e4d..f31cd879e1 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,27 @@ # @techdocs/cli +## 1.8.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 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..c468a1a30c 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.3", "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..5050211099 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-api-docs +## 0.11.8-next.3 + +### Patch Changes + +- 6582799: Add `tableOptions` to all tables and additionally `title` to API tables. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + +## 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..e0831b2d98 100644 --- a/plugins/api-docs/api-report-alpha.md +++ b/plugins/api-docs/api-report-alpha.md @@ -3,8 +3,17 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) @@ -13,7 +22,346 @@ const _default: BackstagePlugin< root: RouteRef; }, { - registerApi: ExternalRouteRef; + registerApi: ExternalRouteRef; + }, + { + 'nav-item:api-docs': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined + >; + 'api:api-docs/config': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'config' + >; + 'page:api-docs': ExtensionDefinition< + { + initiallySelectedFilter: 'all' | 'owned' | 'starred' | undefined; + } & { + path: string | undefined; + }, + { + initiallySelectedFilter?: 'all' | 'owned' | 'starred' | undefined; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'page', + undefined, + undefined + >; + 'entity-card:api-docs/has-apis': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-apis' + >; + 'entity-card:api-docs/definition': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'definition' + >; + 'entity-card:api-docs/consumed-apis': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'consumed-apis' + >; + 'entity-card:api-docs/provided-apis': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'provided-apis' + >; + 'entity-card:api-docs/consuming-components': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'consuming-components' + >; + 'entity-card:api-docs/providing-components': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'providing-components' + >; + 'entity-content:api-docs/definition': ExtensionDefinition< + { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-content', + undefined, + 'definition' + >; + 'entity-content:api-docs/apis': ExtensionDefinition< + { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-content', + undefined, + 'apis' + >; } >; export default _default; diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index 2fc852777b..b3d82fc991 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -9,6 +9,7 @@ import { ApiEntity } from '@backstage/catalog-model'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { ComponentEntity } from '@backstage/catalog-model'; import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; @@ -16,6 +17,7 @@ import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; +import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; @@ -88,12 +90,15 @@ export type AsyncApiDefinitionWidgetProps = { // @public (undocumented) export const ConsumedApisCard: (props: { variant?: InfoCardVariants; + title?: string; columns?: TableColumn[]; + tableOptions?: TableOptions; }) => React_2.JSX.Element; // @public (undocumented) export const ConsumingComponentsCard: (props: { variant?: InfoCardVariants; + columns?: TableColumn[]; }) => React_2.JSX.Element; // @public @@ -118,29 +123,37 @@ export const EntityApiDefinitionCard: () => JSX_2.Element; // @public (undocumented) export const EntityConsumedApisCard: (props: { variant?: InfoCardVariants | undefined; + title?: string | undefined; columns?: TableColumn[] | undefined; + tableOptions?: TableOptions<{}> | undefined; }) => JSX_2.Element; // @public (undocumented) export const EntityConsumingComponentsCard: (props: { variant?: InfoCardVariants | undefined; + columns?: TableColumn[] | undefined; }) => JSX_2.Element; // @public (undocumented) export const EntityHasApisCard: (props: { variant?: InfoCardVariants | undefined; + title?: string | undefined; columns?: TableColumn[] | undefined; + tableOptions?: TableOptions<{}> | undefined; }) => JSX_2.Element; // @public (undocumented) export const EntityProvidedApisCard: (props: { variant?: InfoCardVariants | undefined; + title?: string | undefined; columns?: TableColumn[] | undefined; + tableOptions?: TableOptions<{}> | undefined; }) => JSX_2.Element; // @public (undocumented) export const EntityProvidingComponentsCard: (props: { variant?: InfoCardVariants | undefined; + columns?: TableColumn[] | undefined; }) => JSX_2.Element; // @public (undocumented) @@ -156,7 +169,9 @@ export type GraphQlDefinitionWidgetProps = { // @public (undocumented) export const HasApisCard: (props: { variant?: InfoCardVariants; + title?: string; columns?: TableColumn[]; + tableOptions?: TableOptions; }) => React_2.JSX.Element; // @public (undocumented) @@ -185,12 +200,15 @@ export type PlainApiDefinitionWidgetProps = { // @public (undocumented) export const ProvidedApisCard: (props: { variant?: InfoCardVariants; + title?: string; columns?: TableColumn[]; + tableOptions?: TableOptions; }) => React_2.JSX.Element; // @public (undocumented) export const ProvidingComponentsCard: (props: { variant?: InfoCardVariants; + columns?: TableColumn[]; }) => React_2.JSX.Element; // @public (undocumented) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4f88e8874a..b0f045cade 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.3", "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/api-docs/src/alpha.tsx b/plugins/api-docs/src/alpha.tsx index 48dae28cdd..b38bfc6c91 100644 --- a/plugins/api-docs/src/alpha.tsx +++ b/plugins/api-docs/src/alpha.tsx @@ -18,12 +18,11 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; import { - createApiExtension, + ApiBlueprint, + NavItemBlueprint, + PageBlueprint, createApiFactory, - createNavItemExtension, - createPageExtension, - createPlugin, - createSchemaFromZod, + createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { @@ -31,10 +30,6 @@ import { convertLegacyRouteRef, } from '@backstage/core-compat-api'; -import { - createEntityCardExtension, - createEntityContentExtension, -} from '@backstage/plugin-catalog-react/alpha'; import { ApiEntity, parseEntityRef, @@ -46,164 +41,193 @@ import { rootRoute, registerComponentRouteRef } from './routes'; import { apiDocsConfigRef } from './config'; import { AppIcon } from '@backstage/core-components'; -const apiDocsNavItem = createNavItemExtension({ - title: 'APIs', - routeRef: convertLegacyRouteRef(rootRoute), - icon: () => compatWrapper(), -}); +import { + EntityCardBlueprint, + EntityContentBlueprint, +} from '@backstage/plugin-catalog-react/alpha'; -const apiDocsConfigApi = createApiExtension({ - factory: createApiFactory({ - api: apiDocsConfigRef, - deps: {}, - factory: () => { - const definitionWidgets = defaultDefinitionWidgets(); - return { - getApiDefinitionWidget: (apiEntity: ApiEntity) => { - return definitionWidgets.find(d => d.type === apiEntity.spec.type); - }, - }; - }, - }), -}); - -const apiDocsExplorerPage = createPageExtension({ - defaultPath: '/api-docs', - routeRef: convertLegacyRouteRef(rootRoute), - // Mapping DefaultApiExplorerPageProps to config - configSchema: createSchemaFromZod(z => - z.object({ - path: z.string().default('/api-docs'), - initiallySelectedFilter: z.enum(['owned', 'starred', 'all']).optional(), - // Ommiting columns and actions for now as their types are too complex to map to zod - }), - ), - loader: ({ config }) => - import('./components/ApiExplorerPage').then(m => - compatWrapper( - , - ), - ), -}); - -const apiDocsHasApisEntityCard = createEntityCardExtension({ - name: 'has-apis', - // Ommiting configSchema for now - // We are skipping variants and columns are too complex to map to zod - // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 - filter: entity => { - return ( - entity.kind === 'Component' && - entity.relations?.some( - ({ type, targetRef }) => - type.toLocaleLowerCase('en-US') === RELATION_HAS_PART && - parseEntityRef(targetRef).kind === 'API', - )!! - ); +const apiDocsNavItem = NavItemBlueprint.make({ + params: { + title: 'APIs', + routeRef: convertLegacyRouteRef(rootRoute), + icon: () => compatWrapper(), }, - loader: () => - import('./components/ApisCards').then(m => - compatWrapper(), - ), }); -const apiDocsDefinitionEntityCard = createEntityCardExtension({ +const apiDocsConfigApi = ApiBlueprint.make({ + name: 'config', + params: { + factory: createApiFactory({ + api: apiDocsConfigRef, + deps: {}, + factory: () => { + const definitionWidgets = defaultDefinitionWidgets(); + return { + getApiDefinitionWidget: (apiEntity: ApiEntity) => { + return definitionWidgets.find(d => d.type === apiEntity.spec.type); + }, + }; + }, + }), + }, +}); + +const apiDocsExplorerPage = PageBlueprint.makeWithOverrides({ + config: { + schema: { + // Ommiting columns and actions for now as their types are too complex to map to zod + initiallySelectedFilter: z => + z.enum(['owned', 'starred', 'all']).optional(), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + defaultPath: '/api-docs', + routeRef: convertLegacyRouteRef(rootRoute), + loader: () => + import('./components/ApiExplorerPage').then(m => + compatWrapper( + , + ), + ), + }); + }, +}); + +const apiDocsHasApisEntityCard = EntityCardBlueprint.make({ + name: 'has-apis', + params: { + // Ommiting configSchema for now + // We are skipping variants and columns are too complex to map to zod + // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 + filter: entity => { + return ( + entity.kind === 'Component' && + entity.relations?.some( + ({ type, targetRef }) => + type.toLocaleLowerCase('en-US') === RELATION_HAS_PART && + parseEntityRef(targetRef).kind === 'API', + )!! + ); + }, + loader: () => + import('./components/ApisCards').then(m => + compatWrapper(), + ), + }, +}); + +const apiDocsDefinitionEntityCard = EntityCardBlueprint.make({ name: 'definition', - filter: 'kind:api', - loader: () => - import('./components/ApiDefinitionCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:api', + loader: () => + import('./components/ApiDefinitionCard').then(m => + compatWrapper(), + ), + }, }); -const apiDocsConsumedApisEntityCard = createEntityCardExtension({ +const apiDocsConsumedApisEntityCard = EntityCardBlueprint.make({ name: 'consumed-apis', - // Ommiting configSchema for now - // We are skipping variants and columns are too complex to map to zod - // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 - filter: 'kind:component', - loader: () => - import('./components/ApisCards').then(m => - compatWrapper(), - ), + params: { + // Ommiting configSchema for now + // We are skipping variants and columns are too complex to map to zod + // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 + filter: 'kind:component', + loader: () => + import('./components/ApisCards').then(m => + compatWrapper(), + ), + }, }); -const apiDocsProvidedApisEntityCard = createEntityCardExtension({ +const apiDocsProvidedApisEntityCard = EntityCardBlueprint.make({ name: 'provided-apis', - // Ommiting configSchema for now - // We are skipping variants and columns are too complex to map to zod - // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 - filter: 'kind:component', - loader: () => - import('./components/ApisCards').then(m => - compatWrapper(), - ), + params: { + // Ommiting configSchema for now + // We are skipping variants and columns are too complex to map to zod + // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 + filter: 'kind:component', + loader: () => + import('./components/ApisCards').then(m => + compatWrapper(), + ), + }, }); -const apiDocsConsumingComponentsEntityCard = createEntityCardExtension({ +const apiDocsConsumingComponentsEntityCard = EntityCardBlueprint.make({ name: 'consuming-components', - // Ommiting configSchema for now - // We are skipping variants - // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 - filter: 'kind:api', - loader: () => - import('./components/ComponentsCards').then(m => - compatWrapper(), - ), + params: { + // Ommiting configSchema for now + // We are skipping variants + // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 + filter: 'kind:api', + loader: () => + import('./components/ComponentsCards').then(m => + compatWrapper(), + ), + }, }); -const apiDocsProvidingComponentsEntityCard = createEntityCardExtension({ +const apiDocsProvidingComponentsEntityCard = EntityCardBlueprint.make({ name: 'providing-components', - // Ommiting configSchema for now - // We are skipping variants - // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 - filter: 'kind:api', - loader: () => - import('./components/ComponentsCards').then(m => - compatWrapper(), - ), + params: { + // Ommiting configSchema for now + // We are skipping variants + // See: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 + filter: 'kind:api', + loader: () => + import('./components/ComponentsCards').then(m => + compatWrapper(), + ), + }, }); -const apiDocsDefinitionEntityContent = createEntityContentExtension({ +const apiDocsDefinitionEntityContent = EntityContentBlueprint.make({ name: 'definition', - defaultPath: '/defintion', - defaultTitle: 'Definition', - filter: 'kind:api', - loader: async () => - import('./components/ApiDefinitionCard').then(m => - compatWrapper( - - - - - , + params: { + defaultPath: '/defintion', + defaultTitle: 'Definition', + filter: 'kind:api', + loader: async () => + import('./components/ApiDefinitionCard').then(m => + compatWrapper( + + + + + , + ), ), - ), + }, }); -const apiDocsApisEntityContent = createEntityContentExtension({ +const apiDocsApisEntityContent = EntityContentBlueprint.make({ name: 'apis', - defaultPath: '/apis', - defaultTitle: 'APIs', - filter: 'kind:component', - loader: async () => - import('./components/ApisCards').then(m => - compatWrapper( - - - - - - - - , + params: { + defaultPath: '/apis', + defaultTitle: 'APIs', + filter: 'kind:component', + loader: async () => + import('./components/ApisCards').then(m => + compatWrapper( + + + + + + + + , + ), ), - ), + }, }); -export default createPlugin({ +export default createFrontendPlugin({ id: 'api-docs', routes: { root: convertLegacyRouteRef(rootRoute), diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx index d5494d1818..f68e6dcb40 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx @@ -30,6 +30,7 @@ import { Link, Progress, TableColumn, + TableOptions, WarningPanel, } from '@backstage/core-components'; @@ -38,9 +39,16 @@ import { */ export const ConsumedApisCard = (props: { variant?: InfoCardVariants; + title?: string; columns?: TableColumn[]; + tableOptions?: TableOptions; }) => { - const { variant = 'gridItem', columns = apiEntityColumns } = props; + const { + variant = 'gridItem', + title = 'Consumed APIs', + columns = apiEntityColumns, + tableOptions = {}, + } = props; const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: RELATION_CONSUMES_API, @@ -48,7 +56,7 @@ export const ConsumedApisCard = (props: { if (loading) { return ( - + ); @@ -56,7 +64,7 @@ export const ConsumedApisCard = (props: { if (error || !entities) { return ( - + @@ -84,6 +92,7 @@ export const ConsumedApisCard = (props: {
    } columns={columns} + tableOptions={tableOptions} entities={entities as ApiEntity[]} /> ); diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx index d58a654176..6810d9d2ba 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx @@ -30,6 +30,7 @@ import { Link, Progress, TableColumn, + TableOptions, WarningPanel, } from '@backstage/core-components'; @@ -46,9 +47,16 @@ const presetColumns: TableColumn[] = [ */ export const HasApisCard = (props: { variant?: InfoCardVariants; + title?: string; columns?: TableColumn[]; + tableOptions?: TableOptions; }) => { - const { variant = 'gridItem', columns = presetColumns } = props; + const { + variant = 'gridItem', + title = 'APIs', + columns = presetColumns, + tableOptions = {}, + } = props; const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: RELATION_HAS_PART, @@ -57,7 +65,7 @@ export const HasApisCard = (props: { if (loading) { return ( - + ); @@ -65,7 +73,7 @@ export const HasApisCard = (props: { if (error || !entities) { return ( - + @@ -93,6 +101,7 @@ export const HasApisCard = (props: {
    } columns={columns} + tableOptions={tableOptions} entities={entities as ApiEntity[]} /> ); diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx index 97d0a00b0a..48099df375 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx @@ -30,6 +30,7 @@ import { Link, Progress, TableColumn, + TableOptions, WarningPanel, } from '@backstage/core-components'; @@ -38,9 +39,16 @@ import { */ export const ProvidedApisCard = (props: { variant?: InfoCardVariants; + title?: string; columns?: TableColumn[]; + tableOptions?: TableOptions; }) => { - const { variant = 'gridItem', columns = apiEntityColumns } = props; + const { + variant = 'gridItem', + title = 'Provided APIs', + columns = apiEntityColumns, + tableOptions = {}, + } = props; const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: RELATION_PROVIDES_API, @@ -48,7 +56,7 @@ export const ProvidedApisCard = (props: { if (loading) { return ( - + ); @@ -56,7 +64,7 @@ export const ProvidedApisCard = (props: { if (error || !entities) { return ( - + @@ -84,6 +92,7 @@ export const ProvidedApisCard = (props: {
    } columns={columns} + tableOptions={tableOptions} entities={entities as ApiEntity[]} /> ); diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx index 5feb1c96e4..8e461bb08a 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx @@ -31,6 +31,7 @@ import { InfoCardVariants, Link, Progress, + TableColumn, WarningPanel, } from '@backstage/core-components'; @@ -39,8 +40,10 @@ import { */ export const ConsumingComponentsCard = (props: { variant?: InfoCardVariants; + columns?: TableColumn[]; }) => { - const { variant = 'gridItem' } = props; + const { variant = 'gridItem', columns = EntityTable.componentEntityColumns } = + props; const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: RELATION_API_CONSUMED_BY, @@ -82,7 +85,7 @@ export const ConsumingComponentsCard = (props: {
    } - columns={EntityTable.componentEntityColumns} + columns={columns} entities={entities as ComponentEntity[]} /> ); diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx index ef7f2e1071..33dc7217d4 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx @@ -31,14 +31,17 @@ import { InfoCardVariants, Link, Progress, + TableColumn, WarningPanel, } from '@backstage/core-components'; /** @public */ export const ProvidingComponentsCard = (props: { variant?: InfoCardVariants; + columns?: TableColumn[]; }) => { - const { variant = 'gridItem' } = props; + const { variant = 'gridItem', columns = EntityTable.componentEntityColumns } = + props; const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: RELATION_API_PROVIDED_BY, @@ -80,7 +83,7 @@ export const ProvidingComponentsCard = (props: {
    } - columns={EntityTable.componentEntityColumns} + columns={columns} entities={entities as ComponentEntity[]} /> ); diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index c0a59fca0e..d4ac37c95c 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-app-backend +## 0.3.72-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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 2bd66c7ca0..016ecd079d 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.3", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index efd406fc95..009209d362 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-app-node +## 0.1.23-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config-loader@1.9.0-next.2 + +## 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..972cbe1a87 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.3", "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..f850656652 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-app-visualizer +## 0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 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..cab8b113b6 100644 --- a/plugins/app-visualizer/api-report.md +++ b/plugins/app-visualizer/api-report.md @@ -3,10 +3,63 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) -const visualizerPlugin: BackstagePlugin<{}, {}>; +const visualizerPlugin: BackstagePlugin< + {}, + {}, + { + 'page:app-visualizer': ExtensionDefinition< + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + undefined + >; + 'nav-item:app-visualizer': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined + >; + } +>; 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..4efcf0faf0 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.3", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", @@ -38,11 +38,11 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1" + "@material-ui/icons": "^4.9.1", + "@types/react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "workspace:^", - "@types/react": "^16.13.1 || ^17.0.0" + "@backstage/cli": "workspace:^" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0", diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 2356f38cfa..d22c2d94db 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -20,9 +20,9 @@ import { ExtensionDataRef, RouteRef, coreExtensionData, - createApiExtension, - createNavItemExtension, - createThemeExtension, + ApiBlueprint, + NavItemBlueprint, + ThemeBlueprint, useRouteRef, } from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; @@ -66,9 +66,9 @@ const getOutputColor = createOutputColorGenerator( [coreExtensionData.reactElement.id]: colors.green[500], [coreExtensionData.routePath.id]: colors.yellow[500], [coreExtensionData.routeRef.id]: colors.purple[500], - [createApiExtension.factoryDataRef.id]: colors.blue[500], - [createThemeExtension.themeDataRef.id]: colors.lime[500], - [createNavItemExtension.targetDataRef.id]: colors.orange[500], + [ApiBlueprint.dataRefs.factory.id]: colors.blue[500], + [ThemeBlueprint.dataRefs.theme.id]: colors.lime[500], + [NavItemBlueprint.dataRefs.target.id]: colors.orange[500], }, [ @@ -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} ); @@ -329,11 +323,11 @@ function Extension(props: { node: AppNode; depth: number }) { const legendMap = { 'React Element': coreExtensionData.reactElement, - 'Utility API': createApiExtension.factoryDataRef, + 'Utility API': ApiBlueprint.dataRefs.factory, 'Route Path': coreExtensionData.routePath, 'Route Ref': coreExtensionData.routeRef, - 'Nav Target': createNavItemExtension.targetDataRef, - Theme: createThemeExtension.themeDataRef, + 'Nav Target': NavItemBlueprint.dataRefs.target, + Theme: ThemeBlueprint.dataRefs.theme, }; function Legend() { diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index 9a726ecc99..0e18b692e8 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -15,31 +15,37 @@ */ import { - createNavItemExtension, - createPageExtension, - createPlugin, + createFrontendPlugin, createRouteRef, + NavItemBlueprint, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import VisualizerIcon from '@material-ui/icons/Visibility'; import React from 'react'; const rootRouteRef = createRouteRef(); -const appVisualizerPage = createPageExtension({ - defaultPath: '/visualizer', - routeRef: rootRouteRef, - loader: () => - import('./components/AppVisualizerPage').then(m => ), +const appVisualizerPage = PageBlueprint.make({ + params: { + defaultPath: '/visualizer', + routeRef: rootRouteRef, + loader: () => + import('./components/AppVisualizerPage').then(m => ( + + )), + }, }); -export const appVisualizerNavItem = createNavItemExtension({ - title: 'Visualizer', - icon: VisualizerIcon, - routeRef: rootRouteRef, +export const appVisualizerNavItem = NavItemBlueprint.make({ + params: { + title: 'Visualizer', + icon: VisualizerIcon, + routeRef: rootRouteRef, + }, }); /** @public */ -export const visualizerPlugin = createPlugin({ +export const visualizerPlugin = createFrontendPlugin({ id: 'app-visualizer', extensions: [appVisualizerPage, appVisualizerNavItem], }); diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index 74b7d191c4..5c99e93a17 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..e35b322c34 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.3", "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..d100c055de 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.1.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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 cb1649a23e..c012a4006d 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.3", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index cfc576d6e1..a118b1c4c5 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.1.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..65173f3e63 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.3", "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..7fabd638bd 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.1.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..b3747e3892 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.3", "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..33c617c996 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.2.0-next.3 + +### Minor Changes + +- 75d026a: Support for Cloudflare Custom Headers and Custom Cookie Auth Name + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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/config.d.ts b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts index 1300eb8c57..21b839b8d7 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts @@ -27,6 +27,8 @@ export interface Config { token: string; subject: string; }>; + jwtHeaderName?: string; + authorizationCookieName?: string; signIn?: { resolvers: Array< | { resolver: 'emailLocalPartMatchingUserEntityName' } diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 2cd60873d3..69a59cc8f5 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.2.0-next.3", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts index 9efab07ba5..07e9860d98 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts @@ -239,4 +239,89 @@ describe('helpers', () => { token: token, }); }); + + it('works for regular tokens, through jwtHeaderName header', async () => { + jest.useFakeTimers({ + now: 1600000004000, + }); + + const helper = AuthHelper.fromConfig( + new ConfigReader({ + teamName: 'mock-team', + jwtHeaderName: 'X-Auth-Token', + }), + { cache }, + ); + const token = await tokenFactory.userToken(); + const request = createRequest({ + headers: { ['X-Auth-Token']: token }, + }); + + const expected = { + cfIdentity: { + email: 'hello@example.com', + groups: [{ id: '123', email: 'foo@bar.com', name: 'foo' }], + id: '1234567890', + name: 'User Name', + }, + claims: { + iss: `https://mock-team.cloudflareaccess.com`, + sub: '1234567890', + name: 'User Name', + iat: 1600000000, + exp: 1600000005, + }, + expiresInSeconds: 5, + }; + + await expect(helper.authenticate(request)).resolves.toEqual({ + ...expected, + token: token, + }); + expect(cache.set).toHaveBeenCalledTimes(1); + expect(cache.set.mock.calls[0][0]).toBe( + 'providers/cloudflare-access/profile-v1/1234567890', + ); + expect(JSON.parse(cache.set.mock.calls[0][1] as string)).toEqual(expected); + }); + + it('works for regular tokens, through authorizationCookieName cookie name', async () => { + jest.useFakeTimers({ + now: 1600000004000, + }); + + const helper = AuthHelper.fromConfig( + new ConfigReader({ + teamName: 'mock-team', + authorizationCookieName: 'CF_Auth', + }), + { cache }, + ); + const token = await tokenFactory.userToken(); + const request = createRequest({ + cookies: { CF_Auth: token }, + }); + + const expected = { + cfIdentity: { + email: 'hello@example.com', + groups: [{ id: '123', email: 'foo@bar.com', name: 'foo' }], + id: '1234567890', + name: 'User Name', + }, + claims: { + iss: `https://mock-team.cloudflareaccess.com`, + sub: '1234567890', + name: 'User Name', + iat: 1600000000, + exp: 1600000005, + }, + expiresInSeconds: 5, + }; + + await expect(helper.authenticate(request)).resolves.toEqual({ + ...expected, + token: token, + }); + }); }); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts index c1a14ae6b4..3077258dd6 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.ts @@ -40,6 +40,10 @@ export class AuthHelper { options?: { cache?: CacheService }, ): AuthHelper { const teamName = config.getString('teamName'); + const jwtHeaderName = + config.getOptionalString('jwtHeaderName') ?? CF_JWT_HEADER; + const authorizationCookieName = + config.getOptionalString('authorizationCookieName') ?? COOKIE_AUTH_NAME; const serviceTokens = ( config.getOptionalConfigArray('serviceTokens') ?? [] )?.map(cfg => { @@ -53,12 +57,21 @@ export class AuthHelper { new URL(`https://${teamName}.cloudflareaccess.com/cdn-cgi/access/certs`), ); - return new AuthHelper(teamName, serviceTokens, keySet, options?.cache); + return new AuthHelper( + teamName, + serviceTokens, + jwtHeaderName, + authorizationCookieName, + keySet, + options?.cache, + ); } private constructor( private readonly teamName: string, private readonly serviceTokens: ServiceToken[], + private readonly jwtHeaderName: string, + private readonly authorizationCookieName: string, private readonly keySet: ReturnType, private readonly cache?: CacheService, ) {} @@ -66,15 +79,16 @@ export class AuthHelper { async authenticate(req: express.Request): Promise { // JWTs generated by Access are available in a request header as // Cf-Access-Jwt-Assertion and as cookies as CF_Authorization. - let jwt = req.header(CF_JWT_HEADER); + let jwt = req.header(this.jwtHeaderName); if (!jwt) { - jwt = req.cookies.CF_Authorization; + jwt = req.cookies[this.authorizationCookieName]; } if (!jwt) { // Only throw if both are not provided by Cloudflare Access since either // can be used. throw new AuthenticationError( - `Missing ${CF_JWT_HEADER} from Cloudflare Access`, + `Missing ${this.jwtHeaderName} and + ${this.authorizationCookieName} from Cloudflare Access`, ); } @@ -155,8 +169,8 @@ export class AuthHelper { ): Promise { const headers = new Headers(); // set both headers just the way inbound responses are set - headers.set(CF_JWT_HEADER, jwt); - headers.set('cookie', `${COOKIE_AUTH_NAME}=${jwt}`); + headers.set(this.jwtHeaderName, jwt); + headers.set('cookie', `${this.authorizationCookieName}=${jwt}`); try { const res = await fetch( `https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`, diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index 6d000cede5..b36137aca8 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.2.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts index 452f9819c3..4ca426d10e 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -21,26 +21,24 @@ export interface Config { * Configuration for the Google Cloud Platform Identity-Aware Proxy (IAP) auth provider. */ gcpIap?: { - [authEnv: string]: { - /** - * The audience to use when validating incoming JWT tokens. - * See https://backstage.io/docs/auth/google/gcp-iap-auth - */ - audience: string; + /** + * The audience to use when validating incoming JWT tokens. + * See https://backstage.io/docs/auth/google/gcp-iap-auth + */ + audience: string; - /** - * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`. - */ - jwtHeader?: string; + /** + * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`. + */ + jwtHeader?: string; - signIn?: { - resolvers: Array< - | { resolver: 'emailMatchingUserEntityAnnotation' } - | { resolver: 'idMatchingUserEntityAnnotation' } - | { resolver: 'emailLocalPartMatchingUserEntityName' } - | { resolver: 'emailMatchingUserEntityProfileEmail' } - >; - }; + signIn?: { + resolvers: Array< + | { resolver: 'emailMatchingUserEntityAnnotation' } + | { resolver: 'idMatchingUserEntityAnnotation' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; }; }; }; diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index 098416f347..33fb69e9aa 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.3", "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..1b7fba93cb 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..398fc3ddc8 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.3", "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..f1f3e51923 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..515d89e711 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.3", "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..b12ecd0cd2 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..9d6dc93ed3 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.3", "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..b562085594 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..bc7e381591 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.3", "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..d50a8bb578 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.1.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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 e80adbac1a..d4b6585b19 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.3", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index 066695c188..ecceed8507 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..093d3b20c0 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.3", "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..71ac4d4bcc 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.1.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..1fae82d754 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.3", "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..7fee83d9b6 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-auth-backend@0.22.10-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..535cc083f8 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.3", "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..fe2579b9ca 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.0.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..9d0d648f85 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.3", "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..918a35e8b6 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.1.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..1f0bb22c08 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.3", "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..1dc73a0cf5 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.1.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..54e4793cbe 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.3", "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..fb11720c49 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..421862aae0 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.3", "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..6d1734174a 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-auth-backend +## 0.22.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.2.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.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.3 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.3 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.3 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.3 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.3 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.3 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.3 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.3 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.3 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.3 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.3 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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 8a3d2966a8..9c7b205352 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.3", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index e8b2a4f4b9..a6c4f29d6f 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-auth-node +## 0.5.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 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 bb0057f42a..3367a32b2a 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.3", "backstage": { "role": "node-library", "pluginId": "auth", 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..103b2d1670 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.3.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + +## 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/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 428a39d934..cf846cf25e 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -17,7 +17,7 @@ import { LocationSpec } from '@backstage/plugin-catalog-common'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public export const ANNOTATION_AWS_ACCOUNT_ID: string; @@ -75,7 +75,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { // @public @deprecated export class AwsS3DiscoveryProcessor implements CatalogProcessor { - constructor(reader: UrlReader); + constructor(reader: UrlReaderService); // (undocumented) getProcessorName(): string; // (undocumented) diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 96c57eb821..deff27597b 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.3", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", @@ -58,6 +58,7 @@ "@aws-sdk/middleware-endpoint": "^3.347.0", "@aws-sdk/types": "^3.347.0", "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/catalog-model": "workspace:^", diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts index 804a17f4f0..70b4ddf2d4 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReaders } from '@backstage/backend-common'; +import { UrlReaders } from '@backstage/backend-defaults/urlReader'; import { ConfigReader } from '@backstage/config'; import { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; import { diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts index d6722d046e..dfff1f5d4f 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { isError } from '@backstage/errors'; import { CatalogProcessor, @@ -34,7 +34,7 @@ import limiterFactory from 'p-limit'; * @deprecated Use the `AwsS3EntityProvider` instead (see https://github.com/backstage/backstage/blob/master/plugins/catalog-backend-module-aws/CHANGELOG.md#014). */ export class AwsS3DiscoveryProcessor implements CatalogProcessor { - constructor(private readonly reader: UrlReader) {} + constructor(private readonly reader: UrlReaderService) {} getProcessorName(): string { return 'AwsS3DiscoveryProcessor'; diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index d7b4c90862..1c193f2913 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.43-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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 39f889cb54..42e7f7c927 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.3", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 17b20064ba..08fc9c0a39 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.2.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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..88c03b3dc6 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.3", "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..e9b5bda880 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.2.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..de0652d01b 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.3", "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..92ea2d3841 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.37-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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 7d6f1a38b3..8ec612797c 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.3", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 0a5ed326fd..c0abacbe42 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.24-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + +## 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..55392c01a1 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.3", "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..85668dace1 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.40-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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 6358590dae..4510d5571a 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.3", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index f0f37b1c03..3043985aab 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.1.18-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..79722394cb 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.3", "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..19fe2410b1 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-catalog-backend-module-github +## 0.6.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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 c6af27c2ce..9c97ad2330 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.3", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts index 002b2bbfd1..79af5ad5ca 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts @@ -665,7 +665,7 @@ describe('GithubEntityProvider', () => { event: EventParams, options?: { branch?: string; catalogFilePath?: string }, ): DeferredEntity[] => { - const url = `${event.eventPayload.repository.url}/blob/${ + const url = `${event.eventPayload.repository.html_url}/blob/${ options?.branch ?? 'main' }/${options?.catalogFilePath ?? 'catalog-info.yaml'}`; return createExpectedEntitiesForUrl(url); @@ -687,6 +687,7 @@ describe('GithubEntityProvider', () => { name: 'test-repo', organization, topics: [], + html_url: `https://github.com/${organization}/test-repo`, url: `https://github.com/${organization}/test-repo`, } as Partial; @@ -702,6 +703,9 @@ describe('GithubEntityProvider', () => { const event = { ref: options?.ref ?? 'refs/heads/main', repository: repo as PushEvent['repository'], + organization: { + login: organization, + }, created: true, deleted: false, forced: false, @@ -960,10 +964,10 @@ describe('GithubEntityProvider', () => { ): EventParams => { const repo = { name: 'test-repo', - url: 'https://github.com/test-org/test-repo', + html_url: 'https://github.com/test-org/test-repo', + url: 'https://api.github.com/repos/test-org/test-repo', default_branch: 'main', master_branch: 'main', - organization: 'test-org', topics: [], archived: action === 'archived', private: action !== 'publicized', @@ -972,6 +976,9 @@ describe('GithubEntityProvider', () => { const event = { action, repository: repo as RepositoryEvent['repository'], + organization: { + login: 'test-org', + }, } as RepositoryEvent; if (action === 'renamed') { @@ -1285,7 +1292,7 @@ describe('GithubEntityProvider', () => { const event = createRepoEvent( 'renamed', ) as EventParams; - const urlOldRepo = `https://github.com/${event.eventPayload.repository.organization}/${event.eventPayload.changes.repository.name.from}/blob/main/catalog-info.yaml`; + const urlOldRepo = `https://github.com/${event.eventPayload.organization?.login}/${event.eventPayload.changes.repository.name.from}/blob/main/catalog-info.yaml`; const expectedEntitiesRemoved = createExpectedEntitiesForUrl(urlOldRepo); @@ -1314,7 +1321,7 @@ describe('GithubEntityProvider', () => { const event = createRepoEvent( 'renamed', ) as EventParams; - const urlOldRepo = `https://github.com/${event.eventPayload.repository.organization}/${event.eventPayload.changes.repository.name.from}/blob/main/catalog-info.yaml`; + const urlOldRepo = `https://github.com/${event.eventPayload.organization?.login}/${event.eventPayload.changes.repository.name.from}/blob/main/catalog-info.yaml`; const expectedEntitiesRemoved = createExpectedEntitiesForUrl(urlOldRepo); const expectedEntitiesAdded = createExpectedEntitiesForEvent(event); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts index daade9660a..38008fe03c 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts @@ -294,7 +294,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ async onEvent(params: EventParams): Promise { - this.logger.debug(`Received event from ${params.topic}`); + this.logger.debug(`Received event for topic ${params.topic}`); if (EVENT_TOPICS.some(topic => topic === params.topic)) { if (!this.connection) { throw new Error('Not initialized'); @@ -323,15 +323,15 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { } private async onPush(event: PushEvent) { - if (this.config.organization !== event.repository.organization) { + if (this.config.organization !== event.organization?.login) { this.logger.debug( - `skipping push event from organization ${event.repository.organization}`, + `skipping push event from organization ${event.organization?.login}`, ); return; } const repoName = event.repository.name; - const repoUrl = event.repository.url; + const repoUrl = event.repository.html_url; this.logger.debug(`handle github:push event for ${repoName} - ${repoUrl}`); const branch = @@ -356,13 +356,13 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { // so we will process the change based in this data // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push const added = this.collectDeferredEntitiesFromCommit( - event.repository.url, + repoUrl, branch, event.commits, (commit: Commit) => [...commit.added], ); const removed = this.collectDeferredEntitiesFromCommit( - event.repository.url, + repoUrl, branch, event.commits, (commit: Commit) => [...commit.removed], @@ -381,14 +381,12 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { keys: [ ...new Set([ ...modified.map( - filePath => - `url:${event.repository.url}/tree/${branch}/${filePath}`, + filePath => `url:${repoUrl}/tree/${branch}/${filePath}`, ), ...modified.map( - filePath => - `url:${event.repository.url}/blob/${branch}/${filePath}`, + filePath => `url:${repoUrl}/blob/${branch}/${filePath}`, ), - `url:${event.repository.url}/tree/${branch}/${catalogPath}`, + `url:${repoUrl}/tree/${branch}/${catalogPath}`, ]), ], }); @@ -408,9 +406,9 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { } private async onRepoChange(event: RepositoryEvent) { - if (this.config.organization !== event.repository.organization) { + if (this.config.organization !== event.organization?.login) { this.logger.debug( - `skipping repository event from organization ${event.repository.organization}`, + `skipping repository event from organization ${event.organization?.login}`, ); return; } @@ -527,7 +525,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { private async onRepoRenamed(event: RepositoryRenamedEvent) { const repository = this.createRepoFromEvent(event); const oldRepoName = event.changes.repository.name.from; - const urlParts = event.repository.url.split('/'); + const urlParts = repository.url.split('/'); urlParts[urlParts.length - 1] = oldRepoName; const oldRepoUrl = urlParts.join('/'); const oldRepository: Repository = { @@ -540,7 +538,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { const matchingTargets = this.matchesFilters([repository]); if (matchingTargets.length === 0) { this.logger.debug( - `skipping repository transferred event for repository ${repository.name} because it didn't match provider filters`, + `skipping repository renamed event for repository ${repository.name} because it didn't match provider filters`, ); return; } @@ -587,7 +585,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { const matchingTargets = this.matchesFilters([repository]); if (matchingTargets.length === 0) { this.logger.debug( - `skipping repository transferred event for repository ${repository.name} because it didn't match provider filters`, + `skipping repository unarchived event for repository ${repository.name} because it didn't match provider filters`, ); return; } @@ -632,7 +630,10 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { private createRepoFromEvent(event: RepositoryEvent | PushEvent): Repository { return { - url: event.repository.url, + // $.repository.url can be a value like + // "https://api.github.com/repos/{org}/{repo}" + // or "https://github.com/{org}/{repo}" + url: event.repository.html_url, name: event.repository.name, defaultBranchRef: event.repository.default_branch, repositoryTopics: event.repository.topics, diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index 33ce83336c..6b28f919c3 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.0.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..54a9a5f7bf 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.3", "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..1705be9ab7 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.3.22-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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 2b44a0b75e..9853904759 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.3", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index d43ff7902d..21d5777de7 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 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/api-report.md b/plugins/catalog-backend-module-incremental-ingestion/api-report.md index 4088b05bb5..92666f3928 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/api-report.md +++ b/plugins/catalog-backend-module-incremental-ingestion/api-report.md @@ -16,7 +16,7 @@ import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; import type { PluginDatabaseManager } from '@backstage/backend-common'; import type { PluginTaskScheduler } from '@backstage/backend-tasks'; import { Router } from 'express'; -import type { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public export type EntityIteratorResult = @@ -91,7 +91,7 @@ export type PluginEnvironment = { database: PluginDatabaseManager; scheduler: PluginTaskScheduler; config: Config; - reader: UrlReader; + reader: UrlReaderService; permissions: PermissionEvaluator; }; ``` diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index 8d3d25188d..60af5a5c46 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.3", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/types.ts b/plugins/catalog-backend-module-incremental-ingestion/src/types.ts index 78309afb36..7c637de841 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/types.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/types.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import type { - PluginDatabaseManager, - UrlReader, -} from '@backstage/backend-common'; +import type { PluginDatabaseManager } from '@backstage/backend-common'; import type { PluginTaskScheduler, TaskFunction, @@ -32,7 +29,7 @@ import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; import type { DurationObjectUnits } from 'luxon'; import type { Logger } from 'winston'; import { IncrementalIngestionDatabaseManager } from './database/IncrementalIngestionDatabaseManager'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; /** * Ingest entities into the catalog in bite-sized chunks. @@ -192,7 +189,7 @@ export type PluginEnvironment = { database: PluginDatabaseManager; scheduler: PluginTaskScheduler; config: Config; - reader: UrlReader; + reader: UrlReaderService; permissions: PermissionEvaluator; }; diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index a93f095557..6e9c089bef 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.7.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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..d1a1b0079a 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.3", "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..eb3228d949 100644 --- a/plugins/catalog-backend-module-logs/CHANGELOG.md +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-logs +## 0.0.2-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..7485e7be98 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.3", "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..e0c177201b 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.31-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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 a889d6a22a..afeeaeb6b3 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.3", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 8daf794314..af99194b98 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.41-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-backend@1.24.1-next.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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/api-report.md b/plugins/catalog-backend-module-openapi/api-report.md index b1685c3584..05ab2e0561 100644 --- a/plugins/catalog-backend-module-openapi/api-report.md +++ b/plugins/catalog-backend-module-openapi/api-report.md @@ -12,7 +12,7 @@ import { LocationSpec } from '@backstage/plugin-catalog-common'; import { Logger } from 'winston'; import { PlaceholderResolverParams } from '@backstage/plugin-catalog-backend'; import { ScmIntegrations } from '@backstage/integration'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public const catalogModuleJsonSchemaRefPlaceholderResolver: BackendFeatureCompat; @@ -31,14 +31,14 @@ export class OpenApiRefProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrations; logger: Logger; - reader: UrlReader; + reader: UrlReaderService; }); // (undocumented) static fromConfig( config: Config, options: { logger: Logger; - reader: UrlReader; + reader: UrlReaderService; }, ): OpenApiRefProcessor; // (undocumented) diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index c6147804f9..5149d04b9c 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.3", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts index 6b00519b2f..39de58466e 100644 --- a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -29,11 +29,11 @@ import { Logger } from 'winston'; export class OpenApiRefProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrations; private readonly logger: Logger; - private readonly reader: UrlReader; + private readonly reader: UrlReaderService; static fromConfig( config: Config, - options: { logger: Logger; reader: UrlReader }, + options: { logger: Logger; reader: UrlReaderService }, ) { const integrations = ScmIntegrations.fromConfig(config); @@ -46,7 +46,7 @@ export class OpenApiRefProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrations; logger: Logger; - reader: UrlReader; + reader: UrlReaderService; }) { this.integrations = options.integrations; this.logger = options.logger; diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index 924e0a9df0..1900a98283 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.12.5-next.3 + +## 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 fd9384b567..366063e5ca 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.3", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index 262105e62a..0bd3048063 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.1.21-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + +## 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..b61c28b370 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.3", "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..c4c50f7c4a 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.4.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 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..46bd85a47c 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.3", "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..e387fe4d94 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-catalog-backend +## 1.24.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.3 + +## 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/api-report.md b/plugins/catalog-backend/api-report.md index f4c10d01a2..7efa8669e6 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -63,7 +63,7 @@ import { Router } from 'express'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmLocationAnalyzer as ScmLocationAnalyzer_2 } from '@backstage/plugin-catalog-node'; import { TokenManager } from '@backstage/backend-common'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Validators } from '@backstage/catalog-model'; // @public @deprecated @@ -197,7 +197,7 @@ export type CatalogEnvironment = { logger: LoggerService; database: PluginDatabaseManager; config: Config; - reader: UrlReader; + reader: UrlReaderService; permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; discovery?: DiscoveryService; @@ -254,14 +254,14 @@ export class CodeOwnersProcessor implements CatalogProcessor_2 { constructor(options: { integrations: ScmIntegrationRegistry; logger: LoggerService; - reader: UrlReader; + reader: UrlReaderService; }); // (undocumented) static fromConfig( config: Config, options: { logger: LoggerService; - reader: UrlReader; + reader: UrlReaderService; }, ): CodeOwnersProcessor; // (undocumented) @@ -413,7 +413,7 @@ export class PlaceholderProcessor implements CatalogProcessor_2 { // @public (undocumented) export type PlaceholderProcessorOptions = { resolvers: Record; - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrationRegistry; }; @@ -465,7 +465,7 @@ export function transformLegacyPolicyToProcessor( // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor_2 { - constructor(options: { reader: UrlReader; logger: LoggerService }); + constructor(options: { reader: UrlReaderService; logger: LoggerService }); // (undocumented) getProcessorName(): string; // (undocumented) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index cb3cc31cca..c37bef01a5 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.3", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index b4ed33d162..9b8d2a3bbf 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -24,7 +23,7 @@ import { import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { findCodeOwnerByTarget } from './lib'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System']; const ALLOWED_LOCATION_TYPES = ['url']; @@ -33,11 +32,11 @@ const ALLOWED_LOCATION_TYPES = ['url']; export class CodeOwnersProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly logger: LoggerService; - private readonly reader: UrlReader; + private readonly reader: UrlReaderService; static fromConfig( config: Config, - options: { logger: LoggerService; reader: UrlReader }, + options: { logger: LoggerService; reader: UrlReaderService }, ) { const integrations = ScmIntegrations.fromConfig(config); @@ -50,7 +49,7 @@ export class CodeOwnersProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; logger: LoggerService; - reader: UrlReader; + reader: UrlReaderService; }) { this.integrations = options.integrations; this.logger = options.logger; diff --git a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts index 6a5234c94f..155405ddf8 100644 --- a/plugins/catalog-backend/src/modules/codeowners/lib/read.ts +++ b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { NotFoundError } from '@backstage/errors'; import { ScmIntegration } from '@backstage/integration'; import 'core-js/features/promise'; // NOTE: This can be removed when ES2021 is implemented import { resolveCodeOwner } from './resolve'; import { scmCodeOwnersPaths } from './scm'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; export async function readCodeOwners( - reader: UrlReader, + reader: UrlReaderService, sourceUrl: string, codeownersPaths: string[], ): Promise { @@ -49,7 +49,7 @@ export async function readCodeOwners( } export async function findCodeOwnerByTarget( - reader: UrlReader, + reader: UrlReaderService, targetUrl: string, scmIntegration: ScmIntegration, ): Promise { diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index 094f8f1df8..c2c49ea1cb 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -29,11 +28,12 @@ import { textPlaceholderResolver, yamlPlaceholderResolver, } from './PlaceholderProcessor'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); describe('PlaceholderProcessor', () => { - const reader: jest.Mocked = { + const reader: jest.Mocked = { readTree: jest.fn(), search: jest.fn(), readUrl: jest.fn(), diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index f6431cc9f5..bdbb0433d7 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -27,11 +26,12 @@ import { PlaceholderResolverParams, processingResult, } from '@backstage/plugin-catalog-node'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; /** @public */ export type PlaceholderProcessorOptions = { resolvers: Record; - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrationRegistry; }; diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index 7d1578db00..7e895c5344 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { UrlReader, UrlReaders } from '@backstage/backend-common'; import { mockServices, registerMswTestHooks, @@ -31,6 +30,8 @@ import { } from '@backstage/plugin-catalog-node'; import { defaultEntityDataParser } from '../util/parse'; import { UrlReaderProcessor } from './UrlReaderProcessor'; +import { UrlReaders } from '@backstage/backend-defaults/urlReader'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; @@ -192,7 +193,7 @@ describe('UrlReaderProcessor', () => { it('uses search when there are globs', async () => { const logger = mockServices.logger.mock(); - const reader: jest.Mocked = { + const reader: jest.Mocked = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn().mockImplementation(async () => []), diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index 89f9e74685..b623098171 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; import limiterFactory from 'p-limit'; @@ -29,7 +28,7 @@ import { CatalogProcessorResult, processingResult, } from '@backstage/plugin-catalog-node'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; const CACHE_KEY = 'v1'; @@ -47,7 +46,7 @@ type CacheItem = { export class UrlReaderProcessor implements CatalogProcessor { constructor( private readonly options: { - reader: UrlReader; + reader: UrlReaderService; logger: LoggerService; }, ) {} diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index eb8cbcc81a..0de6b12b90 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -115,24 +115,27 @@ export class ProcessorOutputCollector { // Note that at this point, we have only validated the envelope part of // the entity data. Annotations are not part of that, so we have to be - // defensive. If the annotations were malformed (e.g. were not a valid - // object), we just skip over this step and let the full entity - // validation at the next step of processing catch that. + // defensive and report an error if the annotations isn't a valid object, to avoid + // hiding errors when adding location annotations. const annotations = entity.metadata.annotations || {}; - if (typeof annotations === 'object' && !Array.isArray(annotations)) { - const originLocation = getEntityOriginLocationRef(this.parentEntity); - entity = { - ...entity, - metadata: { - ...entity.metadata, - annotations: { - ...annotations, - [ANNOTATION_ORIGIN_LOCATION]: originLocation, - [ANNOTATION_LOCATION]: location, - }, - }, - }; + if (typeof annotations !== 'object' || Array.isArray(annotations)) { + this.errors.push( + new Error('metadata.annotations must be a valid object'), + ); + return; } + const originLocation = getEntityOriginLocationRef(this.parentEntity); + entity = { + ...entity, + metadata: { + ...entity.metadata, + annotations: { + ...annotations, + [ANNOTATION_ORIGIN_LOCATION]: originLocation, + [ANNOTATION_LOCATION]: location, + }, + }, + }; this.deferredEntities.push({ entity, locationKey: location }); } else if (i.type === 'location') { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index aabef51195..344c231f7c 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -18,7 +18,6 @@ import { createLegacyAuthAdapters, HostDiscovery, PluginDatabaseManager, - UrlReader, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { @@ -112,6 +111,7 @@ import { HttpAuthService, LoggerService, PermissionsService, + UrlReaderService, } from '@backstage/backend-plugin-api'; /** @@ -128,7 +128,7 @@ export type CatalogEnvironment = { logger: LoggerService; database: PluginDatabaseManager; config: Config; - reader: UrlReader; + reader: UrlReaderService; permissions: PermissionsService | PermissionAuthorizer; scheduler?: PluginTaskScheduler; discovery?: DiscoveryService; diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 615bcc538e..706659d062 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-common +## 1.0.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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..e1ab5ec74f 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.2", "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..c8a07dba96 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-graph +## 0.4.8-next.4 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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.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..03ee352227 100644 --- a/plugins/catalog-graph/api-report-alpha.md +++ b/plugins/catalog-graph/api-report-alpha.md @@ -3,8 +3,16 @@ > 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 { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Direction } from '@backstage/plugin-catalog-graph'; +import { Entity } from '@backstage/catalog-model'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) @@ -13,13 +21,136 @@ const _default: BackstagePlugin< catalogGraph: RouteRef; }, { - catalogEntity: ExternalRouteRef< + catalogEntity: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, + { + 'entity-card:catalog-graph/relations': ExtensionDefinition< { - name: string; - kind: string; - namespace: string; + kinds: string[] | undefined; + relations: string[] | undefined; + maxDepth: number | undefined; + unidirectional: boolean | undefined; + mergeRelations: boolean | undefined; + direction: Direction | undefined; + relationPairs: [string, string][] | undefined; + zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + curve: 'curveStepBefore' | 'curveMonotoneX' | undefined; + title: string | undefined; + height: number | undefined; + } & { + filter: string | undefined; }, - true + { + height?: number | undefined; + curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; + direction?: Direction | undefined; + title?: string | undefined; + zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + relations?: string[] | undefined; + maxDepth?: number | undefined; + kinds?: string[] | undefined; + unidirectional?: boolean | undefined; + mergeRelations?: boolean | undefined; + relationPairs?: [string, string][] | undefined; + } & { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'entity-card', + undefined, + 'relations' + >; + 'page:catalog-graph': ExtensionDefinition< + { + selectedKinds: string[] | undefined; + selectedRelations: string[] | undefined; + rootEntityRefs: string[] | undefined; + maxDepth: number | undefined; + unidirectional: boolean | undefined; + mergeRelations: boolean | undefined; + direction: Direction | undefined; + showFilters: boolean | undefined; + curve: 'curveStepBefore' | 'curveMonotoneX' | undefined; + kinds: string[] | undefined; + relations: string[] | undefined; + relationPairs: [string, string][] | undefined; + zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + } & { + path: string | undefined; + }, + { + curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; + direction?: Direction | undefined; + zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + relations?: string[] | undefined; + rootEntityRefs?: string[] | undefined; + maxDepth?: number | undefined; + kinds?: string[] | undefined; + unidirectional?: boolean | undefined; + mergeRelations?: boolean | undefined; + relationPairs?: [string, string][] | undefined; + selectedRelations?: string[] | undefined; + selectedKinds?: string[] | undefined; + showFilters?: boolean | undefined; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'page', + undefined, + undefined >; } >; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 04fe835f5c..c206df00b4 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.4", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index 6d8429332e..8186fd3b96 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -16,87 +16,77 @@ import React from 'react'; import { - createPageExtension, - createPlugin, - createSchemaFromZod, + createFrontendPlugin, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import { compatWrapper, convertLegacyRouteRef, } from '@backstage/core-compat-api'; -import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { catalogGraphRouteRef, catalogEntityRouteRef } from './routes'; -import { Direction } from './components'; +import { Direction } from '@backstage/plugin-catalog-graph'; -function getEntityGraphRelationsConfigSchema( - z: Parameters[0]>[0], -) { - // Mapping EntityRelationsGraphProps to config - // The classname and render functions are configurable only via extension overrides - return z.object({ - kinds: z.array(z.string()).optional(), - relations: z.array(z.string()).optional(), - maxDepth: z.number().optional(), - unidirectional: z.boolean().optional(), - mergeRelations: z.boolean().optional(), - direction: z.nativeEnum(Direction).optional(), - relationPairs: z.array(z.tuple([z.string(), z.string()])).optional(), - zoom: z.enum(['enabled', 'disabled', 'enable-on-click']).optional(), - curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), - }); -} - -const CatalogGraphEntityCard = createEntityCardExtension({ +const CatalogGraphEntityCard = EntityCardBlueprint.makeWithOverrides({ name: 'relations', - configSchema: createSchemaFromZod(z => - z - .object({ - // Filter is a config required to all entity cards - filter: z.string().optional(), - title: z.string().optional(), - height: z.number().optional(), - // Skipping a "variant" config for now, defaulting to "gridItem" in the component - // For more details, see this comment: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 - }) - .merge(getEntityGraphRelationsConfigSchema(z)), - ), - loader: async ({ config: { filter, ...props } }) => - import('./components/CatalogGraphCard').then(m => - compatWrapper(), - ), + config: { + schema: { + kinds: z => z.array(z.string()).optional(), + relations: z => z.array(z.string()).optional(), + maxDepth: z => z.number().optional(), + unidirectional: z => z.boolean().optional(), + mergeRelations: z => z.boolean().optional(), + direction: z => z.nativeEnum(Direction).optional(), + relationPairs: z => z.array(z.tuple([z.string(), z.string()])).optional(), + zoom: z => z.enum(['enabled', 'disabled', 'enable-on-click']).optional(), + curve: z => z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), + // Skipping a "variant" config for now, defaulting to "gridItem" in the component + // For more details, see this comment: https://github.com/backstage/backstage/pull/22619#discussion_r1477333252 + title: z => z.string().optional(), + height: z => z.number().optional(), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + loader: async () => + import('./components/CatalogGraphCard').then(m => + compatWrapper(), + ), + }); + }, }); -const CatalogGraphPage = createPageExtension({ - defaultPath: '/catalog-graph', - routeRef: convertLegacyRouteRef(catalogGraphRouteRef), - configSchema: createSchemaFromZod(z => - z.object({ - // Path is a default config required to all pages - path: z.string().default('/catalog-graph'), - // Mapping intialState prop to config, these are the initial filter values, as opposed to configuration of the available filter values - initialState: z - .object({ - selectedKinds: z.array(z.string()).optional(), - selectedRelations: z.array(z.string()).optional(), - rootEntityRefs: z.array(z.string()).optional(), - maxDepth: z.number().optional(), - unidirectional: z.boolean().optional(), - mergeRelations: z.boolean().optional(), - direction: z.nativeEnum(Direction).optional(), - showFilters: z.boolean().optional(), - curve: z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), - }) - .merge(getEntityGraphRelationsConfigSchema(z)) - .optional(), - }), - ), - loader: ({ config: { path, ...props } }) => - import('./components/CatalogGraphPage').then(m => - compatWrapper(), - ), +const CatalogGraphPage = PageBlueprint.makeWithOverrides({ + config: { + schema: { + selectedKinds: z => z.array(z.string()).optional(), + selectedRelations: z => z.array(z.string()).optional(), + rootEntityRefs: z => z.array(z.string()).optional(), + maxDepth: z => z.number().optional(), + unidirectional: z => z.boolean().optional(), + mergeRelations: z => z.boolean().optional(), + direction: z => z.nativeEnum(Direction).optional(), + showFilters: z => z.boolean().optional(), + curve: z => z.enum(['curveStepBefore', 'curveMonotoneX']).optional(), + kinds: z => z.array(z.string()).optional(), + relations: z => z.array(z.string()).optional(), + relationPairs: z => z.array(z.tuple([z.string(), z.string()])).optional(), + zoom: z => z.enum(['enabled', 'disabled', 'enable-on-click']).optional(), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + defaultPath: '/catalog-graph', + routeRef: convertLegacyRouteRef(catalogGraphRouteRef), + loader: () => + import('./components/CatalogGraphPage').then(m => + compatWrapper(), + ), + }); + }, }); -export default createPlugin({ +export default createFrontendPlugin({ id: 'catalog-graph', routes: { catalogGraph: convertLegacyRouteRef(catalogGraphRouteRef), diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 43e132823a..e042262603 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-catalog-import +## 0.12.2-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + +## 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..3129d13e07 100644 --- a/plugins/catalog-import/api-report-alpha.md +++ b/plugins/catalog-import/api-report-alpha.md @@ -3,7 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/core-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) @@ -11,7 +16,43 @@ const _default: BackstagePlugin< { importPage: RouteRef; }, - {} + {}, + { + 'api:catalog-import': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + undefined + >; + 'page:catalog-import': ExtensionDefinition< + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + undefined + >; + } >; export default _default; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 397d3c92d9..31433aa1da 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.3", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index 795e697874..cf63c92338 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -25,9 +25,9 @@ import { convertLegacyRouteRef, } from '@backstage/core-compat-api'; import { - createApiExtension, - createPageExtension, - createPlugin, + createFrontendPlugin, + PageBlueprint, + ApiBlueprint, } from '@backstage/frontend-plugin-api'; import { scmAuthApiRef, @@ -40,47 +40,51 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; // TODO: It's currently possible to override the import page with a custom one. We need to decide // whether this type of override is typically done with an input or by overriding the entire extension. -const catalogImportPage = createPageExtension({ - defaultPath: '/catalog-import', - routeRef: convertLegacyRouteRef(rootRouteRef), - loader: () => - import('./components/ImportPage').then(m => - compatWrapper(), - ), +const catalogImportPage = PageBlueprint.make({ + params: { + defaultPath: '/catalog-import', + routeRef: convertLegacyRouteRef(rootRouteRef), + loader: () => + import('./components/ImportPage').then(m => + compatWrapper(), + ), + }, }); -const catalogImportApi = createApiExtension({ - factory: createApiFactory({ - api: catalogImportApiRef, - deps: { - discoveryApi: discoveryApiRef, - scmAuthApi: scmAuthApiRef, - fetchApi: fetchApiRef, - scmIntegrationsApi: scmIntegrationsApiRef, - catalogApi: catalogApiRef, - configApi: configApiRef, - }, - factory: ({ - discoveryApi, - scmAuthApi, - fetchApi, - scmIntegrationsApi, - catalogApi, - configApi, - }) => - new CatalogImportClient({ +const catalogImportApi = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: catalogImportApiRef, + deps: { + discoveryApi: discoveryApiRef, + scmAuthApi: scmAuthApiRef, + fetchApi: fetchApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + catalogApi: catalogApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, scmAuthApi, - scmIntegrationsApi, fetchApi, + scmIntegrationsApi, catalogApi, configApi, - }), - }), + }) => + new CatalogImportClient({ + discoveryApi, + scmAuthApi, + scmIntegrationsApi, + fetchApi, + catalogApi, + configApi, + }), + }), + }, }); /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'catalog-import', extensions: [catalogImportApi, catalogImportPage], routes: { diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index f276e0a93e..8ff814b976 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-catalog-node +## 1.12.5-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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..34ce2760ee 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.3", "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..dd7b1ae3fe 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-catalog-react +## 1.12.3-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.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-common@1.0.26-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + +## 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..c5dc632946 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -6,16 +6,19 @@ /// import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { PortableSchema } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; import { ResolvedExtensionInputs } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export const catalogExtensionData: { entityContentTitle: ConfigurableExtensionDataRef< string, @@ -92,7 +95,7 @@ export const catalogReactTranslationRef: TranslationRef< } >; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export function createEntityCardExtension< TConfig extends { filter?: string; @@ -115,9 +118,17 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export function createEntityContentExtension< TInputs extends AnyExtensionInputMap, >(options: { @@ -148,6 +159,122 @@ export function createEntityContentExtension< filter?: string | undefined; title?: string | undefined; path?: string | undefined; + }, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; + +// @alpha +export const EntityCardBlueprint: ExtensionBlueprint< + 'entity-card', + undefined, + undefined, + { + loader: () => Promise; + filter?: string | ((entity: Entity) => boolean) | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + { + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; + } +>; + +// @alpha +export const EntityContentBlueprint: ExtensionBlueprint< + 'entity-content', + undefined, + undefined, + { + loader: () => Promise; + defaultPath: string; + defaultTitle: string; + routeRef?: RouteRef | undefined; + filter?: string | ((entity: Entity) => boolean) | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + { + title: ConfigurableExtensionDataRef< + string, + 'catalog.entity-content-title', + {} + >; + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; } >; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index fdbd942529..ef5d924200 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.3", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", @@ -29,7 +29,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -37,7 +37,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" @@ -85,6 +85,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx new file mode 100644 index 0000000000..0cb7cdead8 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx @@ -0,0 +1,178 @@ +/* + * 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 { EntityCardBlueprint } from './EntityCardBlueprint'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor, screen } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; + +describe('EntityCardBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = EntityCardBlueprint.make({ + name: 'test', + params: { + filter: 'has:labels', + loader: async () =>
    im a card
    , + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "entity-content:catalog/overview", + "input": "cards", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "entity-card", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-function", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-expression", + "optional": [Function], + "toString": [Function], + }, + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output the correct filter output', () => { + const mockFilter = (_entity: Entity) => true; + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + loader: async () =>
    Test!
    , + filter: 'test', + }, + }), + ).get(EntityCardBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + loader: async () =>
    Test!
    , + }, + }), + { config: { filter: 'test' } }, + ).get(EntityCardBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + filter: mockFilter, + loader: async () =>
    Test!
    , + }, + }), + ).get(EntityCardBlueprint.dataRefs.filterFunction), + ).toBe(mockFilter); + }); + + it('should allow overriding config and inputs', async () => { + const extension = EntityCardBlueprint.makeWithOverrides({ + name: 'test', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + mock: z => z.string(), + }, + }, + factory(originalFactory, { inputs, config }) { + return originalFactory({ + loader: async () => ( +
    + config: {config.mock} +
    + {inputs.mock.map((i, k) => ( +
    {i.get(coreExtensionData.reactElement)}
    + ))} +
    +
    + ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'entity-card:test', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
    im a mock
    )]; + }, + }); + + createExtensionTester(extension, { config: { mock: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx new file mode 100644 index 0000000000..c3153b6059 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx @@ -0,0 +1,75 @@ +/* + * 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 { + ExtensionBoundary, + coreExtensionData, + createExtensionBlueprint, +} from '@backstage/frontend-plugin-api'; +import { catalogExtensionData } from '../extensions'; + +/** + * @alpha + * A blueprint for creating cards for the entity pages in the catalog. + */ +export const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', + attachTo: { id: 'entity-content:catalog/overview', input: 'cards' }, + output: [ + coreExtensionData.reactElement, + catalogExtensionData.entityFilterFunction.optional(), + catalogExtensionData.entityFilterExpression.optional(), + ], + dataRefs: { + filterFunction: catalogExtensionData.entityFilterFunction, + filterExpression: catalogExtensionData.entityFilterExpression, + }, + config: { + schema: { + filter: z => z.string().optional(), + }, + }, + *factory( + { + loader, + filter, + }: { + loader: () => Promise; + filter?: + | typeof catalogExtensionData.entityFilterFunction.T + | typeof catalogExtensionData.entityFilterExpression.T; + }, + { node, config }, + ) { + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.reactElement( + + + , + ); + + if (config.filter) { + yield catalogExtensionData.entityFilterExpression(config.filter); + } else if (typeof filter === 'string') { + yield catalogExtensionData.entityFilterExpression(filter); + } else if (typeof filter === 'function') { + yield catalogExtensionData.entityFilterFunction(filter); + } + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx new file mode 100644 index 0000000000..5d91f402de --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx @@ -0,0 +1,226 @@ +/* + * 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 { EntityContentBlueprint } from './EntityContentBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createRouteRef, +} from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { waitFor, screen } from '@testing-library/react'; + +describe('EntityContentBlueprint', () => { + it('should return an extension with sane defaults', () => { + const extension = EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
    Test!
    , + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string", + }, + "path": { + "type": "string", + }, + "title": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "entity-content", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-function", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-expression", + "optional": [Function], + "toString": [Function], + }, + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should emit the correct defaults', () => { + const mockRouteRef = createRouteRef(); + const extension = EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + routeRef: mockRouteRef, + loader: async () =>
    Test!
    , + }, + }); + + const tester = createExtensionTester(extension); + + // todo(blam): route paths are always set to / in the createExtensionTester. This will work eventually. + // expect(tester.get(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.get(coreExtensionData.routeRef)).toBe(mockRouteRef); + expect(tester.get(EntityContentBlueprint.dataRefs.title)).toBe('Test'); + }); + + it('should emit the correct filter output', () => { + const mockFilter = (_entity: Entity) => true; + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
    Test!
    , + filter: 'test', + }, + }), + ).get(EntityContentBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
    Test!
    , + }, + }), + { config: { filter: 'test' } }, + ).get(EntityContentBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + filter: mockFilter, + loader: async () =>
    Test!
    , + }, + }), + ).get(EntityContentBlueprint.dataRefs.filterFunction), + ).toBe(mockFilter); + }); + + it('should allow overriding config and inputs', async () => { + const extension = EntityContentBlueprint.makeWithOverrides({ + name: 'test', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + mock: z => z.string(), + }, + }, + factory(originalFactory, { inputs, config }) { + return originalFactory({ + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () => ( +
    + config: {config.mock} +
    + {inputs.mock.map((i, k) => ( +
    {i.get(coreExtensionData.reactElement)}
    + ))} +
    +
    + ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'entity-content:test', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
    im a mock
    )]; + }, + }); + + createExtensionTester(extension, { config: { mock: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx new file mode 100644 index 0000000000..7c7a6c43b7 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.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 React, { lazy } from 'react'; +import { + coreExtensionData, + createExtensionBlueprint, + ExtensionBoundary, + RouteRef, +} from '@backstage/frontend-plugin-api'; +import { catalogExtensionData } from '../extensions'; + +/** + * @alpha + * Creates an EntityContent extension. + */ +export const EntityContentBlueprint = createExtensionBlueprint({ + kind: 'entity-content', + attachTo: { id: 'page:catalog/entity', input: 'contents' }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + catalogExtensionData.entityContentTitle, + coreExtensionData.routeRef.optional(), + catalogExtensionData.entityFilterFunction.optional(), + catalogExtensionData.entityFilterExpression.optional(), + ], + dataRefs: { + title: catalogExtensionData.entityContentTitle, + filterFunction: catalogExtensionData.entityFilterFunction, + filterExpression: catalogExtensionData.entityFilterExpression, + }, + config: { + schema: { + path: z => z.string().optional(), + title: z => z.string().optional(), + filter: z => z.string().optional(), + }, + }, + *factory( + { + loader, + defaultPath, + defaultTitle, + filter, + routeRef, + }: { + loader: () => Promise; + defaultPath: string; + defaultTitle: string; + routeRef?: RouteRef; + filter?: + | typeof catalogExtensionData.entityFilterFunction.T + | typeof catalogExtensionData.entityFilterExpression.T; + }, + { node, config }, + ) { + const path = config.path ?? defaultPath; + const title = config.title ?? defaultTitle; + + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.reactElement( + + + , + ); + + yield coreExtensionData.routePath(path); + + yield catalogExtensionData.entityContentTitle(title); + + if (routeRef) { + yield coreExtensionData.routeRef(routeRef); + } + + if (config.filter) { + yield catalogExtensionData.entityFilterExpression(config.filter); + } else if (typeof filter === 'string') { + yield catalogExtensionData.entityFilterExpression(filter); + } else if (typeof filter === 'function') { + yield catalogExtensionData.entityFilterFunction(filter); + } + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts new file mode 100644 index 0000000000..dccaf28cd9 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { EntityCardBlueprint } from './EntityCardBlueprint'; +export { EntityContentBlueprint } from './EntityContentBlueprint'; diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha/extensions.tsx similarity index 91% rename from plugins/catalog-react/src/alpha.tsx rename to plugins/catalog-react/src/alpha/extensions.tsx index e81b8e5ebf..e527edc355 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha/extensions.tsx @@ -28,13 +28,16 @@ import { import React, { lazy } from 'react'; import { Entity } from '@backstage/catalog-model'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { Expand } from '../../../packages/frontend-plugin-api/src/types'; +import { Expand } from '../../../../packages/frontend-plugin-api/src/types'; -export { useEntityPermission } from './hooks/useEntityPermission'; -export { isOwnerOf } from './utils'; -export * from './translation'; +export { useEntityPermission } from '../hooks/useEntityPermission'; +export { isOwnerOf } from '../utils'; +export * from '../translation'; -/** @alpha */ +/** + * @alpha + * @deprecated use `dataRefs` property on either {@link EntityCardBlueprint} or {@link EntityContentBlueprint} instead + */ export const catalogExtensionData = { entityContentTitle: createExtensionDataRef().with({ id: 'catalog.entity-content-title', @@ -48,7 +51,10 @@ export const catalogExtensionData = { }; // TODO: Figure out how to merge with provided config schema -/** @alpha */ +/** + * @alpha + * @deprecated use {@link EntityCardBlueprint} instead + */ export function createEntityCardExtension< TConfig extends { filter?: string }, TInputs extends AnyExtensionInputMap, @@ -83,7 +89,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(), @@ -110,7 +116,10 @@ export function createEntityCardExtension< }); } -/** @alpha */ +/** + * @alpha + * @deprecated use {@link EntityContentBlueprint} instead + */ export function createEntityContentExtension< TInputs extends AnyExtensionInputMap, >(options: { @@ -137,7 +146,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/packages/backend-app-api/src/services/implementations/auth/index.ts b/plugins/catalog-react/src/alpha/index.ts similarity index 90% rename from packages/backend-app-api/src/services/implementations/auth/index.ts rename to plugins/catalog-react/src/alpha/index.ts index 1b55d46a83..132b288008 100644 --- a/packages/backend-app-api/src/services/implementations/auth/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { authServiceFactory } from './authServiceFactory'; +export * from './blueprints'; +export * from './extensions'; diff --git a/plugins/catalog-react/src/setupTests.ts b/plugins/catalog-react/src/setupTests.ts index 963c0f188b..dae4360fd3 100644 --- a/plugins/catalog-react/src/setupTests.ts +++ b/plugins/catalog-react/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; 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-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index 2c2eb03f5e..effa10a3ab 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 8ce8c8b0d3..1e4409c2c8 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.7-next.0", + "version": "0.2.7-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 0a229ff17a..a302ea72be 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-catalog +## 1.22.0-next.3 + +### Patch Changes + +- 6582799: Add `tableOptions` to all tables and additionally `title` to API tables. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @backstage/catalog-client@1.6.6-next.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-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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 f79d65394a..386024e0c0 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -5,14 +5,41 @@ ```ts /// +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { PortableSchema } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; +import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +// @alpha +export const CatalogFilterBlueprint: ExtensionBlueprint< + 'catalog-filter', + undefined, + undefined, + { + loader: () => Promise; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + never +>; + // @alpha (undocumented) export const catalogTranslationRef: TranslationRef< 'catalog', @@ -98,7 +125,7 @@ export const catalogTranslationRef: TranslationRef< } >; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, TConfig, @@ -108,7 +135,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< @@ -121,23 +156,611 @@ const _default: BackstagePlugin< }>; }, { - viewTechDoc: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + createComponent: ExternalRouteRef; + createFromTemplate: ExternalRouteRef<{ + namespace: string; + templateName: string; + }>; + unregisterRedirect: ExternalRouteRef; + }, + { + 'nav-item:catalog': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined >; - createComponent: ExternalRouteRef; - createFromTemplate: ExternalRouteRef< - { - namespace: string; - templateName: string; - }, - true + 'api:catalog/starred-entities': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'starred-entities' + >; + 'api:catalog/entity-presentation': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'entity-presentation' + >; + 'entity-card:catalog/about': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'about' + >; + 'entity-card:catalog/links': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'links' + >; + 'entity-card:catalog/labels': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'labels' + >; + 'entity-card:catalog/depends-on-components': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'depends-on-components' + >; + 'entity-card:catalog/depends-on-resources': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'depends-on-resources' + >; + 'entity-card:catalog/has-components': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-components' + >; + 'entity-card:catalog/has-resources': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-resources' + >; + 'entity-card:catalog/has-subcomponents': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-subcomponents' + >; + 'entity-card:catalog/has-subdomains': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-subdomains' + >; + 'entity-card:catalog/has-systems': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'has-systems' + >; + 'entity-content:catalog/overview': ExtensionDefinition< + { + [x: string]: any; + } & { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + [x: string]: any; + } & { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + { + cards: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + } + >; + }, + 'entity-content', + undefined, + 'overview' + >; + 'catalog-filter:catalog/tag': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'catalog-filter', + undefined, + 'tag' + >; + 'catalog-filter:catalog/kind': ExtensionDefinition< + { + initialFilter: string; + }, + { + initialFilter?: string | undefined; + }, + ConfigurableExtensionDataRef, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'catalog-filter', + undefined, + 'kind' + >; + 'catalog-filter:catalog/type': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'catalog-filter', + undefined, + 'type' + >; + 'catalog-filter:catalog/mode': ExtensionDefinition< + { + mode: 'all' | 'owners-only' | undefined; + }, + { + mode?: 'all' | 'owners-only' | undefined; + }, + ConfigurableExtensionDataRef, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'catalog-filter', + undefined, + 'mode' + >; + 'catalog-filter:catalog/namespace': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'catalog-filter', + undefined, + 'namespace' + >; + 'catalog-filter:catalog/lifecycle': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'catalog-filter', + undefined, + 'lifecycle' + >; + 'catalog-filter:catalog/processing-status': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'catalog-filter', + undefined, + 'processing-status' + >; + 'catalog-filter:catalog/list': ExtensionDefinition< + { + initialFilter: 'all' | 'owned' | 'starred'; + }, + { + initialFilter?: 'all' | 'owned' | 'starred' | undefined; + }, + ConfigurableExtensionDataRef, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'catalog-filter', + undefined, + 'list' + >; + 'page:catalog': ExtensionDefinition< + { + [x: string]: any; + } & { + path: string | undefined; + }, + { + [x: string]: any; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + filters: ExtensionInput< + ConfigurableExtensionDataRef, + { + singleton: false; + optional: false; + } + >; + }, + 'page', + undefined, + undefined + >; + 'page:catalog/entity': ExtensionDefinition< + { + [x: string]: any; + } & { + path: string | undefined; + }, + { + [x: string]: any; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + contents: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-content-title', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + } + >; + }, + 'page', + undefined, + 'entity' + >; + 'search-result-list-item:catalog': ExtensionDefinition< + { + noTrack: boolean; + }, + { + noTrack?: boolean | undefined; + }, + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + {}, + 'search-result-list-item', + undefined, + undefined >; - unregisterRedirect: ExternalRouteRef; } >; export default _default; diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 95333a6451..541cb08887 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -33,6 +33,7 @@ import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-rea import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; +import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; @@ -304,6 +305,10 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { // @public (undocumented) export interface DependencyOfComponentsCardProps { + // (undocumented) + columns?: TableColumn[]; + // (undocumented) + tableOptions?: TableOptions; // (undocumented) title?: string; // (undocumented) @@ -521,6 +526,10 @@ export function hasCatalogProcessingErrors( // @public (undocumented) export interface HasComponentsCardProps { + // (undocumented) + columns?: TableColumn[]; + // (undocumented) + tableOptions?: TableOptions; // (undocumented) title?: string; // (undocumented) @@ -540,6 +549,10 @@ export function hasRelationWarnings( // @public (undocumented) export interface HasResourcesCardProps { + // (undocumented) + columns?: TableColumn[]; + // (undocumented) + tableOptions?: TableOptions; // (undocumented) title?: string; // (undocumented) @@ -548,6 +561,8 @@ export interface HasResourcesCardProps { // @public (undocumented) export interface HasSubcomponentsCardProps { + // (undocumented) + columns?: TableColumn[]; // (undocumented) tableOptions?: TableOptions; // (undocumented) @@ -568,6 +583,10 @@ export interface HasSubdomainsCardProps { // @public (undocumented) export interface HasSystemsCardProps { + // (undocumented) + columns?: TableColumn[]; + // (undocumented) + tableOptions?: TableOptions; // (undocumented) title?: string; // (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index e747170032..b8f8864f3c 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.3", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", @@ -29,7 +29,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -37,7 +37,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.ts" + "src/alpha/index.ts" ], "package.json": [ "package.json" @@ -90,6 +90,7 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", diff --git a/plugins/catalog/src/alpha.ts b/plugins/catalog/src/alpha.ts deleted file mode 100644 index b78a5e6225..0000000000 --- a/plugins/catalog/src/alpha.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -export * from './alpha/index'; -export { default } from './alpha/index'; -export { catalogTranslationRef } from './translation'; -export * from './translation'; diff --git a/plugins/catalog/src/alpha/apis.tsx b/plugins/catalog/src/alpha/apis.tsx index 92880ba728..d0f80c3aec 100644 --- a/plugins/catalog/src/alpha/apis.tsx +++ b/plugins/catalog/src/alpha/apis.tsx @@ -21,7 +21,7 @@ import { storageApiRef, } from '@backstage/core-plugin-api'; import { CatalogClient } from '@backstage/catalog-client'; -import { createApiExtension } from '@backstage/frontend-plugin-api'; +import { ApiBlueprint } from '@backstage/frontend-plugin-api'; import { catalogApiRef, entityPresentationApiRef, @@ -32,33 +32,42 @@ import { DefaultStarredEntitiesApi, } from '../apis'; -export const catalogApi = createApiExtension({ - factory: createApiFactory({ - api: catalogApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new CatalogClient({ discoveryApi, fetchApi }), - }), +export const catalogApi = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: catalogApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new CatalogClient({ discoveryApi, fetchApi }), + }), + }, }); -export const catalogStarredEntitiesApi = createApiExtension({ - factory: createApiFactory({ - api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), - }), +export const catalogStarredEntitiesApi = ApiBlueprint.make({ + name: 'starred-entities', + params: { + factory: createApiFactory({ + api: starredEntitiesApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => + new DefaultStarredEntitiesApi({ storageApi }), + }), + }, }); -export const entityPresentationApi = createApiExtension({ - factory: createApiFactory({ - api: entityPresentationApiRef, - deps: { catalogApiImp: catalogApiRef }, - factory: ({ catalogApiImp }) => - DefaultEntityPresentationApi.create({ catalogApi: catalogApiImp }), - }), +export const entityPresentationApi = ApiBlueprint.make({ + name: 'entity-presentation', + params: { + factory: createApiFactory({ + api: entityPresentationApiRef, + deps: { catalogApiImp: catalogApiRef }, + factory: ({ catalogApiImp }) => + DefaultEntityPresentationApi.create({ catalogApi: catalogApiImp }), + }), + }, }); export default [catalogApi, catalogStarredEntitiesApi, entityPresentationApi]; diff --git a/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.test.tsx b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.test.tsx new file mode 100644 index 0000000000..9cf8b65957 --- /dev/null +++ b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 { CatalogFilterBlueprint } from './CatalogFilterBlueprint'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor, screen } from '@testing-library/react'; + +describe('CatalogFilterBlueprint', () => { + it('should create an extension with sane defaults', () => { + const extension = CatalogFilterBlueprint.make({ + params: { + loader: async () =>
    , + }, + }); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:catalog", + "input": "filters", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "catalog-filter", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should allow overrding of inputs and config', async () => { + const extension = CatalogFilterBlueprint.makeWithOverrides({ + name: 'test-name', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + test: z => z.string(), + }, + }, + factory(originalFactory, { config, inputs }) { + return originalFactory({ + loader: async () => ( +
    + config: {config.test} +
    + {inputs.mock.map((i, k) => ( +
    {i.get(coreExtensionData.reactElement)}
    + ))} +
    +
    + ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'catalog-filter:test-name', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
    im a mock
    )]; + }, + }); + + createExtensionTester(extension, { config: { test: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.tsx b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.tsx new file mode 100644 index 0000000000..e8d6df55d2 --- /dev/null +++ b/plugins/catalog/src/alpha/blueprints/CatalogFilterBlueprint.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, { lazy } from 'react'; +import { + ExtensionBoundary, + coreExtensionData, + createExtensionBlueprint, +} from '@backstage/frontend-plugin-api'; + +/** + * Creates Catalog Filter Extensions + * @alpha + */ +export const CatalogFilterBlueprint = createExtensionBlueprint({ + kind: 'catalog-filter', + attachTo: { id: 'page:catalog', input: 'filters' }, + output: [coreExtensionData.reactElement], + factory(params: { loader: () => Promise }, { node }) { + const ExtensionComponent = lazy(() => + params.loader().then(element => ({ default: () => element })), + ); + return [ + coreExtensionData.reactElement( + + + , + ), + ]; + }, +}); diff --git a/packages/backend-app-api/src/services/implementations/httpAuth/index.ts b/plugins/catalog/src/alpha/blueprints/index.ts similarity index 90% rename from packages/backend-app-api/src/services/implementations/httpAuth/index.ts rename to plugins/catalog/src/alpha/blueprints/index.ts index edd7e53026..a3a7bcb33f 100644 --- a/packages/backend-app-api/src/services/implementations/httpAuth/index.ts +++ b/plugins/catalog/src/alpha/blueprints/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { httpAuthServiceFactory } from './httpAuthServiceFactory'; +export { CatalogFilterBlueprint } from './CatalogFilterBlueprint'; diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx index d8a218f8f3..85d911ef3b 100644 --- a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx +++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx @@ -23,7 +23,10 @@ import { createExtension, } from '@backstage/frontend-plugin-api'; -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link CatalogFilterBlueprint} instead + */ export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, TConfig, diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 332bd49c55..5f8ee16f87 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -15,89 +15,116 @@ */ import React from 'react'; -import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { compatWrapper } from '@backstage/core-compat-api'; -export const catalogAboutEntityCard = createEntityCardExtension({ +export const catalogAboutEntityCard = EntityCardBlueprint.make({ name: 'about', - loader: async () => - import('../components/AboutCard').then(m => - compatWrapper(), - ), + params: { + loader: async () => + import('../components/AboutCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogLinksEntityCard = createEntityCardExtension({ +export const catalogLinksEntityCard = EntityCardBlueprint.make({ name: 'links', - filter: 'has:links', - loader: async () => - import('../components/EntityLinksCard').then(m => - compatWrapper(), - ), + params: { + filter: 'has:links', + loader: async () => + import('../components/EntityLinksCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogLabelsEntityCard = createEntityCardExtension({ +export const catalogLabelsEntityCard = EntityCardBlueprint.make({ name: 'labels', - filter: 'has:labels', - loader: async () => - import('../components/EntityLabelsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'has:labels', + loader: async () => + import('../components/EntityLabelsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ +export const catalogDependsOnComponentsEntityCard = EntityCardBlueprint.make({ name: 'depends-on-components', - loader: async () => - import('../components/DependsOnComponentsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:component', + loader: async () => + import('../components/DependsOnComponentsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ +export const catalogDependsOnResourcesEntityCard = EntityCardBlueprint.make({ name: 'depends-on-resources', - loader: async () => - import('../components/DependsOnResourcesCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:component', + loader: async () => + import('../components/DependsOnResourcesCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasComponentsEntityCard = createEntityCardExtension({ +export const catalogHasComponentsEntityCard = EntityCardBlueprint.make({ name: 'has-components', - loader: async () => - import('../components/HasComponentsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:system', + loader: async () => + import('../components/HasComponentsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasResourcesEntityCard = createEntityCardExtension({ +export const catalogHasResourcesEntityCard = EntityCardBlueprint.make({ name: 'has-resources', - loader: async () => - import('../components/HasResourcesCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:system', + loader: async () => + import('../components/HasResourcesCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ +export const catalogHasSubcomponentsEntityCard = EntityCardBlueprint.make({ name: 'has-subcomponents', - loader: async () => - import('../components/HasSubcomponentsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:component', + loader: async () => + import('../components/HasSubcomponentsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ +export const catalogHasSubdomainsEntityCard = EntityCardBlueprint.make({ name: 'has-subdomains', - loader: async () => - import('../components/HasSubdomainsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:domain', + loader: async () => + import('../components/HasSubdomainsCard').then(m => + compatWrapper(), + ), + }, }); -export const catalogHasSystemsEntityCard = createEntityCardExtension({ +export const catalogHasSystemsEntityCard = EntityCardBlueprint.make({ name: 'has-systems', - loader: async () => - import('../components/HasSystemsCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:domain', + loader: async () => + import('../components/HasSystemsCard').then(m => + compatWrapper(), + ), + }, }); export default [ diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 75c3fef49f..259b92492c 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -19,27 +19,38 @@ import { coreExtensionData, createExtensionInput, } from '@backstage/frontend-plugin-api'; -import { - createEntityContentExtension, - catalogExtensionData, -} from '@backstage/plugin-catalog-react/alpha'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; -export const catalogOverviewEntityContent = createEntityContentExtension({ - name: 'overview', - defaultPath: '/', - defaultTitle: 'Overview', - disabled: false, - inputs: { - cards: createExtensionInput({ - element: coreExtensionData.reactElement, - filterFunction: catalogExtensionData.entityFilterFunction.optional(), - filterExpression: catalogExtensionData.entityFilterExpression.optional(), - }), - }, - loader: async ({ inputs }) => - import('./EntityOverviewPage').then(m => ( - c.output)} /> - )), -}); +export const catalogOverviewEntityContent = + EntityContentBlueprint.makeWithOverrides({ + name: 'overview', + inputs: { + cards: createExtensionInput([ + coreExtensionData.reactElement, + EntityContentBlueprint.dataRefs.filterFunction.optional(), + EntityContentBlueprint.dataRefs.filterExpression.optional(), + ]), + }, + factory: (originalFactory, { inputs }) => { + return originalFactory({ + defaultPath: '/', + defaultTitle: 'Overview', + loader: async () => + import('./EntityOverviewPage').then(m => ( + ({ + element: c.get(coreExtensionData.reactElement), + filterFunction: c.get( + EntityContentBlueprint.dataRefs.filterFunction, + ), + filterExpression: c.get( + EntityContentBlueprint.dataRefs.filterExpression, + ), + }))} + /> + )), + }); + }, + }); export default [catalogOverviewEntityContent]; diff --git a/plugins/catalog/src/alpha/filter/matrchers/createHasMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createHasMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createHasMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createHasMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createHasMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createIsMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createIsMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createIsMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createIsMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createIsMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createKindMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createKindMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createKindMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createKindMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createKindMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createTypeMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createTypeMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/createTypeMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/createTypeMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matrchers/types.ts b/plugins/catalog/src/alpha/filter/matchers/types.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matrchers/types.ts rename to plugins/catalog/src/alpha/filter/matchers/types.ts diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.ts index a4c17cb8da..4f809690f7 100644 --- a/plugins/catalog/src/alpha/filter/parseFilterExpression.ts +++ b/plugins/catalog/src/alpha/filter/parseFilterExpression.ts @@ -16,11 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; -import { EntityMatcherFn } from './matrchers/types'; -import { createKindMatcher } from './matrchers/createKindMatcher'; -import { createTypeMatcher } from './matrchers/createTypeMatcher'; -import { createIsMatcher } from './matrchers/createIsMatcher'; -import { createHasMatcher } from './matrchers/createHasMatcher'; +import { EntityMatcherFn } from './matchers/types'; +import { createKindMatcher } from './matchers/createKindMatcher'; +import { createTypeMatcher } from './matchers/createTypeMatcher'; +import { createIsMatcher } from './matchers/createIsMatcher'; +import { createHasMatcher } from './matchers/createHasMatcher'; const rootMatcherFactories: Record< string, diff --git a/plugins/catalog/src/alpha/filters.tsx b/plugins/catalog/src/alpha/filters.tsx index 62cf9a1353..3507658731 100644 --- a/plugins/catalog/src/alpha/filters.tsx +++ b/plugins/catalog/src/alpha/filters.tsx @@ -15,97 +15,122 @@ */ import React from 'react'; -import { createCatalogFilterExtension } from './createCatalogFilterExtension'; -import { createSchemaFromZod } from '@backstage/frontend-plugin-api'; +import { CatalogFilterBlueprint } from './blueprints'; -const catalogTagCatalogFilter = createCatalogFilterExtension({ +const catalogTagCatalogFilter = CatalogFilterBlueprint.make({ name: 'tag', - loader: async () => { - const { EntityTagPicker } = await import('@backstage/plugin-catalog-react'); - return ; + params: { + loader: async () => { + const { EntityTagPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, }, }); -const catalogKindCatalogFilter = createCatalogFilterExtension({ +const catalogKindCatalogFilter = CatalogFilterBlueprint.makeWithOverrides({ name: 'kind', - configSchema: createSchemaFromZod(z => - z.object({ - initialFilter: z.string().default('component'), - }), - ), - loader: async ({ config }) => { - const { EntityKindPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; + config: { + schema: { + initialFilter: z => z.string().default('component'), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + loader: async () => { + const { EntityKindPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, + }); }, }); -const catalogTypeCatalogFilter = createCatalogFilterExtension({ +const catalogTypeCatalogFilter = CatalogFilterBlueprint.make({ name: 'type', - loader: async () => { - const { EntityTypePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; + params: { + loader: async () => { + const { EntityTypePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, }, }); -const catalogModeCatalogFilter = createCatalogFilterExtension({ +const catalogModeCatalogFilter = CatalogFilterBlueprint.makeWithOverrides({ name: 'mode', - configSchema: createSchemaFromZod(z => - z.object({ - mode: z.enum(['owners-only', 'all']).optional(), - }), - ), - loader: async ({ config }) => { - const { EntityOwnerPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; + config: { + schema: { + mode: z => z.enum(['owners-only', 'all']).optional(), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + loader: async () => { + const { EntityOwnerPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, + }); }, }); -const catalogNamespaceCatalogFilter = createCatalogFilterExtension({ +const catalogNamespaceCatalogFilter = CatalogFilterBlueprint.make({ name: 'namespace', - loader: async () => { - const { EntityNamespacePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; + params: { + loader: async () => { + const { EntityNamespacePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, }, }); -const catalogLifecycleCatalogFilter = createCatalogFilterExtension({ +const catalogLifecycleCatalogFilter = CatalogFilterBlueprint.make({ name: 'lifecycle', - loader: async () => { - const { EntityLifecyclePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; + params: { + loader: async () => { + const { EntityLifecyclePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, }, }); -const catalogProcessingStatusCatalogFilter = createCatalogFilterExtension({ +const catalogProcessingStatusCatalogFilter = CatalogFilterBlueprint.make({ name: 'processing-status', - loader: async () => { - const { EntityProcessingStatusPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; + params: { + loader: async () => { + const { EntityProcessingStatusPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, }, }); -const catalogListCatalogFilter = createCatalogFilterExtension({ +const catalogListCatalogFilter = CatalogFilterBlueprint.makeWithOverrides({ name: 'list', - configSchema: createSchemaFromZod(z => - z.object({ - initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'), - }), - ), - loader: async ({ config }) => { - const { UserListPicker } = await import('@backstage/plugin-catalog-react'); - return ; + config: { + schema: { + initialFilter: z => z.enum(['owned', 'starred', 'all']).default('owned'), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + loader: async () => { + const { UserListPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, + }); }, }); diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts index 06a78ec6ea..f004c4f19e 100644 --- a/plugins/catalog/src/alpha/index.ts +++ b/plugins/catalog/src/alpha/index.ts @@ -16,3 +16,6 @@ export { default } from './plugin'; export { createCatalogFilterExtension } from './createCatalogFilterExtension'; + +export * from './blueprints'; +export * from './translation'; diff --git a/plugins/catalog/src/alpha/navItems.tsx b/plugins/catalog/src/alpha/navItems.tsx index c1360cc29f..0eb4e6827e 100644 --- a/plugins/catalog/src/alpha/navItems.tsx +++ b/plugins/catalog/src/alpha/navItems.tsx @@ -16,13 +16,15 @@ import HomeIcon from '@material-ui/icons/Home'; import { convertLegacyRouteRef } from '@backstage/core-compat-api'; -import { createNavItemExtension } from '@backstage/frontend-plugin-api'; +import { NavItemBlueprint } from '@backstage/frontend-plugin-api'; import { rootRouteRef } from '../routes'; -export const catalogNavItem = createNavItemExtension({ - routeRef: convertLegacyRouteRef(rootRouteRef), - title: 'Catalog', - icon: HomeIcon, +export const catalogNavItem = NavItemBlueprint.make({ + params: { + routeRef: convertLegacyRouteRef(rootRouteRef), + title: 'Catalog', + icon: HomeIcon, + }, }); export default [catalogNavItem]; diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 5c583e6a19..764acd1fe5 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -20,72 +20,86 @@ import { convertLegacyRouteRef, } from '@backstage/core-compat-api'; import { - createPageExtension, coreExtensionData, createExtensionInput, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import { AsyncEntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { catalogExtensionData } from '@backstage/plugin-catalog-react/alpha'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; import { buildFilterFn } from './filter/FilterWrapper'; -export const catalogPage = createPageExtension({ - defaultPath: '/catalog', - routeRef: convertLegacyRouteRef(rootRouteRef), +export const catalogPage = PageBlueprint.makeWithOverrides({ inputs: { - filters: createExtensionInput({ - element: coreExtensionData.reactElement, - }), + filters: createExtensionInput([coreExtensionData.reactElement]), }, - loader: async ({ inputs }) => { - const { BaseCatalogPage } = await import('../components/CatalogPage'); - const filters = inputs.filters.map(filter => filter.output.element); - return compatWrapper({filters}} />); + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/catalog', + routeRef: convertLegacyRouteRef(rootRouteRef), + loader: async () => { + const { BaseCatalogPage } = await import('../components/CatalogPage'); + const filters = inputs.filters.map(filter => + filter.get(coreExtensionData.reactElement), + ); + return compatWrapper({filters}} />); + }, + }); }, }); -export const catalogEntityPage = createPageExtension({ +export const catalogEntityPage = PageBlueprint.makeWithOverrides({ name: 'entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), inputs: { - contents: createExtensionInput({ - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - title: catalogExtensionData.entityContentTitle, - filterFunction: catalogExtensionData.entityFilterFunction.optional(), - filterExpression: catalogExtensionData.entityFilterExpression.optional(), - }), + contents: createExtensionInput([ + coreExtensionData.reactElement, + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + EntityContentBlueprint.dataRefs.title, + EntityContentBlueprint.dataRefs.filterFunction.optional(), + EntityContentBlueprint.dataRefs.filterExpression.optional(), + ]), }, - loader: async ({ inputs }) => { - const { EntityLayout } = await import('../components/EntityLayout'); - const Component = () => { - return ( - - - {inputs.contents.map(({ output }) => ( - - {output.element} - - ))} - - - ); - }; - return compatWrapper(); + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + loader: async () => { + const { EntityLayout } = await import('../components/EntityLayout'); + const Component = () => { + return ( + + + {inputs.contents.map(output => { + return ( + + {output.get(coreExtensionData.reactElement)} + + ); + })} + + + ); + }; + return compatWrapper(); + }, + }); }, }); diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 9a964c4e46..8a18500043 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -15,7 +15,7 @@ */ import { convertLegacyRouteRefs } from '@backstage/core-compat-api'; -import { createPlugin } from '@backstage/frontend-plugin-api'; +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; @@ -36,7 +36,7 @@ import entityContents from './entityContents'; import searchResultItems from './searchResultItems'; /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'catalog', routes: convertLegacyRouteRefs({ catalogIndex: rootRouteRef, diff --git a/plugins/catalog/src/alpha/searchResultItems.tsx b/plugins/catalog/src/alpha/searchResultItems.tsx index 5233144173..8e140de637 100644 --- a/plugins/catalog/src/alpha/searchResultItems.tsx +++ b/plugins/catalog/src/alpha/searchResultItems.tsx @@ -14,14 +14,16 @@ * limitations under the License. */ -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; -export const catalogSearchResultListItem = createSearchResultListItemExtension({ - predicate: result => result.type === 'software-catalog', - component: () => - import('../components/CatalogSearchResultListItem').then( - m => m.CatalogSearchResultListItem, - ), +export const catalogSearchResultListItem = SearchResultListItemBlueprint.make({ + params: { + predicate: result => result.type === 'software-catalog', + component: () => + import('../components/CatalogSearchResultListItem').then( + m => m.CatalogSearchResultListItem, + ), + }, }); export default [catalogSearchResultListItem]; diff --git a/plugins/catalog/src/translation.ts b/plugins/catalog/src/alpha/translation.ts similarity index 100% rename from plugins/catalog/src/translation.ts rename to plugins/catalog/src/alpha/translation.ts diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index ff5d7fc801..cf4a3c2d43 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -64,7 +64,7 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common import { useSourceTemplateCompoundEntityRef } from './hooks'; import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; import { usePermission } from '@backstage/plugin-permission-react'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 3f781f4759..f6d2d80264 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -33,7 +33,7 @@ import React from 'react'; import { AboutField } from './AboutField'; import { LinksGridList } from '../EntityLinksCard/LinksGridList'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; const useStyles = makeStyles({ description: { diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index db765b9145..a46a552b8d 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -33,7 +33,7 @@ import { import React, { ReactNode } from 'react'; import { createComponentRouteRef } from '../../routes'; import { CatalogTable, CatalogTableRow } from '../CatalogTable'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { CatalogTableColumnsFunc } from '../CatalogTable/types'; diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 1a596f8b9b..f4501bd795 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -27,7 +27,7 @@ import { ResultHighlight, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; const useStyles = makeStyles( diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index e18467249a..1026dc5d26 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -49,7 +49,7 @@ import { CatalogTableColumnsFunc, CatalogTableRow } from './types'; import { PaginatedCatalogTable } from './PaginatedCatalogTable'; import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; /** * Props for {@link CatalogTable}. diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx index 617d009d4f..6c6f811cc1 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx @@ -14,8 +14,15 @@ * limitations under the License. */ -import { RELATION_DEPENDENCY_OF } from '@backstage/catalog-model'; -import { InfoCardVariants } from '@backstage/core-components'; +import { + ComponentEntity, + RELATION_DEPENDENCY_OF, +} from '@backstage/catalog-model'; +import { + InfoCardVariants, + TableColumn, + TableOptions, +} from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, @@ -23,13 +30,15 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface DependencyOfComponentsCardProps { variant?: InfoCardVariants; title?: string; + columns?: TableColumn[]; + tableOptions?: TableOptions; } export function DependencyOfComponentsCard( @@ -39,6 +48,8 @@ export function DependencyOfComponentsCard( const { variant = 'gridItem', title = t('dependencyOfComponentsCard.title'), + columns = componentEntityColumns, + tableOptions = {}, } = props; return ( ); } diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index fe56ea582b..eafa33346d 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -27,7 +27,7 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index 281bfd8766..723b87cc29 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -27,7 +27,7 @@ import { RelatedEntitiesCard, resourceEntityColumns, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 5b740038a1..77f934e7de 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -33,7 +33,7 @@ import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/ import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx index 08496d4819..63247d0c62 100644 --- a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx @@ -19,7 +19,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import MenuItem from '@material-ui/core/MenuItem'; import CancelIcon from '@material-ui/icons/Cancel'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type VisibleType = 'visible' | 'hidden' | 'disable'; diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx index d48b45f054..c4557b6774 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx @@ -25,7 +25,7 @@ import { import { EntityLabelsEmptyState } from './EntityLabelsEmptyState'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx index 16d0a3a897..d78be2d2ce 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx @@ -19,7 +19,7 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const ENTITY_YAML = `metadata: diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index bbc7792acb..e8b957497d 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -53,7 +53,7 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index b5d8269a0b..59e1f6d252 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -23,7 +23,7 @@ import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; /** @public */ export interface EntityLinksCardProps { diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx index 09362385f9..513183677d 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -20,7 +20,7 @@ import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; const ENTITY_YAML = `metadata: name: example diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx index 252005dff5..a033199ffd 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx @@ -20,7 +20,7 @@ import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import { Illo } from './Illo'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx index c9c24a14a1..b1a7afbb50 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -23,7 +23,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'; import React, { useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; interface DeleteEntityDialogProps { diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index 10a10e54fd..eb561f9c19 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -22,7 +22,7 @@ import { useNavigate } from 'react-router-dom'; import { DeleteEntityDialog } from './DeleteEntityDialog'; import { useRouteRef } from '@backstage/core-plugin-api'; import { rootRouteRef } from '../../routes'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index bd49785abe..3c7c5bb2e3 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -31,7 +31,7 @@ import { import { useApi, ApiHolder } from '@backstage/core-plugin-api'; import useAsync from 'react-use/esm/useAsync'; import { SerializedError } from '@backstage/errors'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const errorFilter = (i: EntityStatusItem) => diff --git a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx index 2ce9120bf0..ed816bfc75 100644 --- a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx +++ b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx @@ -26,7 +26,7 @@ import useAsync from 'react-use/esm/useAsync'; import Box from '@material-ui/core/Box'; import { ResponseErrorPanel } from '@backstage/core-components'; import { useApi, ApiHolder } from '@backstage/core-plugin-api'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; async function getRelationWarnings(entity: Entity, catalogApi: CatalogApi) { diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 917543dd12..2fc6cde81b 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import { RELATION_HAS_PART } from '@backstage/catalog-model'; -import { InfoCardVariants } from '@backstage/core-components'; +import { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + InfoCardVariants, + TableColumn, + TableOptions, +} from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, @@ -23,28 +27,36 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasComponentsCardProps { variant?: InfoCardVariants; title?: string; + columns?: TableColumn[]; + tableOptions?: TableOptions; } export function HasComponentsCard(props: HasComponentsCardProps) { const { t } = useTranslationRef(catalogTranslationRef); - const { variant = 'gridItem', title = t('hasComponentsCard.title') } = props; + const { + variant = 'gridItem', + title = t('hasComponentsCard.title'), + columns = componentEntityColumns, + tableOptions = {}, + } = props; return ( ); } diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index d11f10c882..6b90ce0443 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import { RELATION_HAS_PART } from '@backstage/catalog-model'; -import { InfoCardVariants } from '@backstage/core-components'; +import { RELATION_HAS_PART, ResourceEntity } from '@backstage/catalog-model'; +import { + InfoCardVariants, + TableColumn, + TableOptions, +} from '@backstage/core-components'; import React from 'react'; import { asResourceEntities, @@ -23,28 +27,36 @@ import { resourceEntityColumns, resourceEntityHelpLink, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasResourcesCardProps { variant?: InfoCardVariants; title?: string; + columns?: TableColumn[]; + tableOptions?: TableOptions; } export function HasResourcesCard(props: HasResourcesCardProps) { const { t } = useTranslationRef(catalogTranslationRef); - const { variant = 'gridItem', title = t('hasResourcesCard.title') } = props; + const { + variant = 'gridItem', + title = t('hasResourcesCard.title'), + columns = resourceEntityColumns, + tableOptions = {}, + } = props; return ( ); } diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index d81349794a..ceab037a45 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -14,30 +14,36 @@ * limitations under the License. */ -import { RELATION_HAS_PART } from '@backstage/catalog-model'; -import { InfoCardVariants, TableOptions } from '@backstage/core-components'; +import { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + InfoCardVariants, + TableColumn, + TableOptions, +} from '@backstage/core-components'; import React from 'react'; import { asComponentEntities, componentEntityColumns, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasSubcomponentsCardProps { variant?: InfoCardVariants; - tableOptions?: TableOptions; title?: string; + columns?: TableColumn[]; + tableOptions?: TableOptions; } export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) { const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', - tableOptions = {}, title = t('hasSubcomponentsCard.title'), + columns = componentEntityColumns, + tableOptions = {}, } = props; return ( []; + tableOptions?: TableOptions; } export function HasSystemsCard(props: HasSystemsCardProps) { const { t } = useTranslationRef(catalogTranslationRef); - const { variant = 'gridItem', title = t('hasSystemsCard.title') } = props; + const { + variant = 'gridItem', + title = t('hasSystemsCard.title'), + columns = systemEntityColumns, + tableOptions = {}, + } = props; return ( ); } diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index dd1359482d..a2814527cc 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -31,7 +31,18 @@ import { TableColumn, TableOptions, } from '@backstage/core-components'; -import { catalogTranslationRef } from '../../translation'; +import { + asComponentEntities, + asResourceEntities, + asSystemEntities, + componentEntityColumns, + componentEntityHelpLink, + resourceEntityColumns, + resourceEntityHelpLink, + systemEntityColumns, + systemEntityHelpLink, +} from './presets'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ @@ -59,9 +70,9 @@ export type RelatedEntitiesCardProps = { * * @public */ -export function RelatedEntitiesCard( +export const RelatedEntitiesCard = ( props: RelatedEntitiesCardProps, -) { +) => { const { variant = 'gridItem', title, @@ -116,4 +127,14 @@ export function RelatedEntitiesCard( tableOptions={tableOptions} /> ); -} +}; + +RelatedEntitiesCard.componentEntityColumns = componentEntityColumns; +RelatedEntitiesCard.componentEntityHelpLink = componentEntityHelpLink; +RelatedEntitiesCard.asComponentEntities = asComponentEntities; +RelatedEntitiesCard.resourceEntityColumns = resourceEntityColumns; +RelatedEntitiesCard.resourceEntityHelpLink = resourceEntityHelpLink; +RelatedEntitiesCard.asResourceEntities = asResourceEntities; +RelatedEntitiesCard.systemEntityColumns = systemEntityColumns; +RelatedEntitiesCard.systemEntityHelpLink = systemEntityHelpLink; +RelatedEntitiesCard.asSystemEntities = asSystemEntities; diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 5e485d672b..879a5076b1 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -46,7 +46,7 @@ import { } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { catalogTranslationRef } from '../../translation'; +import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ diff --git a/plugins/catalog/src/setupTests.ts b/plugins/catalog/src/setupTests.ts index 963c0f188b..dae4360fd3 100644 --- a/plugins/catalog/src/setupTests.ts +++ b/plugins/catalog/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index 323385dc82..aa952b8e0b 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-devtools-backend +## 0.3.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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 630ea213b8..b4aff3b838 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.3", "backstage": { "role": "backend-plugin", "pluginId": "devtools", 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..a19edbc26a 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-devtools +## 0.1.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + +## 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..f5ca1f98c1 100644 --- a/plugins/devtools/api-report-alpha.md +++ b/plugins/devtools/api-report-alpha.md @@ -3,7 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) @@ -11,7 +17,60 @@ const _default: BackstagePlugin< { root: RouteRef; }, - {} + {}, + { + 'api:devtools': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + undefined + >; + 'page:devtools': ExtensionDefinition< + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + undefined + >; + 'nav-item:devtools': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined + >; + } >; export default _default; diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index a6956161c6..4c72b3a0cf 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.3", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index 0420556504..4c62dd3155 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -16,13 +16,13 @@ import React from 'react'; import { - createApiExtension, createApiFactory, - createNavItemExtension, - createPageExtension, - createPlugin, + createFrontendPlugin, discoveryApiRef, fetchApiRef, + ApiBlueprint, + PageBlueprint, + NavItemBlueprint, } from '@backstage/frontend-plugin-api'; import { devToolsApiRef, DevToolsClient } from '../api'; @@ -34,37 +34,43 @@ import BuildIcon from '@material-ui/icons/Build'; import { rootRouteRef } from '../routes'; /** @alpha */ -export const devToolsApi = createApiExtension({ - factory: createApiFactory({ - api: devToolsApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new DevToolsClient({ discoveryApi, fetchApi }), - }), +export const devToolsApi = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: devToolsApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new DevToolsClient({ discoveryApi, fetchApi }), + }), + }, }); /** @alpha */ -export const devToolsPage = createPageExtension({ - defaultPath: '/devtools', - routeRef: convertLegacyRouteRef(rootRouteRef), - loader: () => - import('../components/DevToolsPage').then(m => - compatWrapper(), - ), +export const devToolsPage = PageBlueprint.make({ + params: { + defaultPath: '/devtools', + routeRef: convertLegacyRouteRef(rootRouteRef), + loader: () => + import('../components/DevToolsPage').then(m => + compatWrapper(), + ), + }, }); /** @alpha */ -export const devToolsNavItem = createNavItemExtension({ - title: 'DevTools', - routeRef: convertLegacyRouteRef(rootRouteRef), - icon: BuildIcon, +export const devToolsNavItem = NavItemBlueprint.make({ + params: { + title: 'DevTools', + routeRef: convertLegacyRouteRef(rootRouteRef), + icon: BuildIcon, + }, }); /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'devtools', routes: { root: convertLegacyRouteRef(rootRouteRef), diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 53a4e8a3fa..f50553f114 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.3.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..32294e7543 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.3", "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..174d3c5635 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..ecbf8a018c 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.3", "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..c1789f2d62 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..0ca12ed4f6 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.3", "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..cbf3353c9f 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..b2737b01c1 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.3", "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..4cce2cf72c 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-events-backend-module-github +## 0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..b84096de5e 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.3", "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..ec17ec1157 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.2.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..e851cdfe9a 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.3", "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..936dc8b1ad 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.33-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..b289b3597b 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.3", "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..02a6fcdba2 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-events-backend +## 0.3.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..3922e370f9 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.3", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index a01292b23e..46908304fc 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-events-node +## 0.3.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + +## 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..43f42fefbf 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.3", "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..5cb7d4bb00 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,25 @@ # @internal/plugin-todo-list-backend +## 1.0.30-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + +## 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..4d6a68a378 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.3", "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..9c6b1a95f6 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-home +## 0.7.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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.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..2efdc19b79 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -3,11 +3,71 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; +import { RouteRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin<{}, {}>; +const _default: BackstagePlugin< + {}, + {}, + { + 'page:home': ExtensionDefinition< + { + [x: string]: any; + } & { + path: string | undefined; + }, + { + [x: string]: any; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + props: ExtensionInput< + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'title', + { + optional: true; + } + >, + { + singleton: true; + optional: true; + } + >; + }, + 'page', + undefined, + undefined + >; + } +>; export default _default; // @alpha (undocumented) diff --git a/plugins/home/package.json b/plugins/home/package.json index 792f7a1525..38f0357eb8 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.3", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index b13c2a2e0e..ce96b18943 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -20,8 +20,8 @@ import { coreExtensionData, createExtensionDataRef, createExtensionInput, - createPageExtension, - createPlugin, + PageBlueprint, + createFrontendPlugin, createRouteRef, } from '@backstage/frontend-plugin-api'; import { compatWrapper } from '@backstage/core-compat-api'; @@ -35,37 +35,40 @@ export const titleExtensionDataRef = createExtensionDataRef().with({ id: 'title', }); -const homePage = createPageExtension({ - defaultPath: '/home', - routeRef: rootRouteRef, +const homePage = PageBlueprint.makeWithOverrides({ inputs: { props: createExtensionInput( - { - children: coreExtensionData.reactElement.optional(), - title: titleExtensionDataRef.optional(), - }, - + [ + coreExtensionData.reactElement.optional(), + titleExtensionDataRef.optional(), + ], { singleton: true, optional: true, }, ), }, - loader: ({ inputs }) => - import('./components/').then(m => - compatWrapper( - , - ), - ), + factory: (originalFactory, { inputs }) => { + return originalFactory({ + defaultPath: '/home', + routeRef: rootRouteRef, + loader: () => + import('./components/').then(m => + compatWrapper( + , + ), + ), + }); + }, }); /** * @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'home', extensions: [homePage], }); diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index c286d49164..529e0416cd 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-kubernetes-backend +## 0.18.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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/package.json b/plugins/kubernetes-backend/package.json index 4b69c912b8..0b1271636b 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.3", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", @@ -99,6 +99,7 @@ }, "devDependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts index cfa89a6bfd..5264e762b6 100644 --- a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -20,10 +20,10 @@ import { mockServices, startTestBackend, } from '@backstage/backend-test-utils'; -import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node'; import { createBackendModule } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; describe('resourcesRoutes', () => { let app: ExtendedHttpServer; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 5edb8845d4..60683a74f3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -56,7 +56,7 @@ import { kubernetesFetcherExtensionPoint, kubernetesServiceLocatorExtensionPoint, } from '@backstage/plugin-kubernetes-node'; -import { ExtendedHttpServer } from '@backstage/backend-app-api'; +import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; describe('API integration tests', () => { let app: ExtendedHttpServer; diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index cab3826e64..11ab293a29 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + - @backstage/plugin-kubernetes-react@0.4.2-next.3 + +## 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..db419b2da3 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.3", "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..391834a694 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-kubernetes-common +## 0.8.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 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..c53f9af5d9 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.2", "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..e24db5bb5d 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-kubernetes-node +## 0.1.17-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + +## 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 f238334824..4873e277c1 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.3", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", @@ -49,6 +49,7 @@ "devDependencies": { "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-kubernetes-backend": "workspace:^", diff --git a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts index 24cd459fde..65e48affc0 100644 --- a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts +++ b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ClusterDetails } from '../types'; import { mockServices, @@ -37,6 +36,7 @@ import { JsonObject } from '@backstage/types'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; describe('Pinniped - tokenCredentialRequest', () => { let app: ExtendedHttpServer; diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 8859563287..59789ef20e 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-kubernetes-react +## 0.4.2-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.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-kubernetes-common@0.8.2-next.2 + +## 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..904ecc81b2 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.3", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx index 6d64b8ad5b..3b131a617c 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.test.tsx @@ -40,6 +40,7 @@ describe('ContainerCard', () => { }, containerSpec: { readinessProbe: {}, + livenessProbe: {}, }, containerStatus: { name: 'some-name', diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx index cabda69b2c..1919bc112d 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/ContainerCard.tsx @@ -35,20 +35,25 @@ const getContainerHealthChecks = ( containerSpec: IContainer, containerStatus: IContainerStatus, ): { [key: string]: boolean } => { - if (containerStatus.state?.terminated?.reason === 'Completed') { - return { - 'not waiting to start': containerStatus.state?.waiting === undefined, - 'no restarts': containerStatus.restartCount === 0, - }; - } - return { + const healthCheck = { 'not waiting to start': containerStatus.state?.waiting === undefined, - started: !!containerStatus.started, - ready: containerStatus.ready, 'no restarts': containerStatus.restartCount === 0, - 'readiness probe set': - containerSpec && containerSpec?.readinessProbe !== undefined, }; + if (containerStatus.state?.terminated?.reason === 'Completed') { + return healthCheck; + } + Object.assign( + healthCheck, + { started: !!containerStatus.started }, + { ready: containerStatus.ready }, + { 'readiness probe set': containerSpec?.readinessProbe !== undefined }, + ); + if (containerSpec && containerSpec?.livenessProbe !== undefined) { + Object.assign(healthCheck, { + 'liveness probe set': containerSpec.livenessProbe, + }); + } + return healthCheck; }; const getCurrentState = (containerStatus: IContainerStatus): string => { diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 384f6574bc..2fd3c5ab4a 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-kubernetes +## 0.11.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.2-next.2 + - @backstage/plugin-kubernetes-react@0.4.2-next.3 + +## 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..6b53443e2a 100644 --- a/plugins/kubernetes/api-report-alpha.md +++ b/plugins/kubernetes/api-report-alpha.md @@ -3,7 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; // @public (undocumented) @@ -11,7 +19,97 @@ const _default: BackstagePlugin< { kubernetes: RouteRef; }, - {} + {}, + { + 'page:kubernetes': ExtensionDefinition< + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + undefined + >; + 'entity-content:kubernetes/kubernetes': ExtensionDefinition< + { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-content', + undefined, + 'kubernetes' + >; + 'api:kubernetes/proxy': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'proxy' + >; + 'api:kubernetes/auth-providers': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'auth-providers' + >; + 'api:kubernetes/cluster-link-formatter': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'cluster-link-formatter' + >; + } >; export default _default; diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index d711c5bdb5..de70299816 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.3", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes/src/alpha/apis.tsx b/plugins/kubernetes/src/alpha/apis.tsx index 6c880d2f53..082a2da6c7 100644 --- a/plugins/kubernetes/src/alpha/apis.tsx +++ b/plugins/kubernetes/src/alpha/apis.tsx @@ -15,7 +15,7 @@ */ import { - createApiExtension, + ApiBlueprint, createApiFactory, discoveryApiRef, fetchApiRef, @@ -40,80 +40,91 @@ import { oneloginAuthApiRef, } from '@backstage/core-plugin-api'; -export const kubernetesApiExtension = createApiExtension({ - factory: createApiFactory({ - api: kubernetesApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - kubernetesAuthProvidersApi: kubernetesAuthProvidersApiRef, - }, - factory: ({ discoveryApi, fetchApi, kubernetesAuthProvidersApi }) => - new KubernetesBackendClient({ - discoveryApi, - fetchApi, - kubernetesAuthProvidersApi, - }), - }), +export const kubernetesApiExtension = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: kubernetesApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + kubernetesAuthProvidersApi: kubernetesAuthProvidersApiRef, + }, + factory: ({ discoveryApi, fetchApi, kubernetesAuthProvidersApi }) => + new KubernetesBackendClient({ + discoveryApi, + fetchApi, + kubernetesAuthProvidersApi, + }), + }), + }, }); -export const kubernetesProxyApi = createApiExtension({ - factory: createApiFactory({ - api: kubernetesProxyApiRef, - deps: { - kubernetesApi: kubernetesApiRef, - }, - factory: ({ kubernetesApi }) => - new KubernetesProxyClient({ - kubernetesApi, - }), - }), +export const kubernetesProxyApi = ApiBlueprint.make({ + name: 'proxy', + params: { + factory: createApiFactory({ + api: kubernetesProxyApiRef, + deps: { + kubernetesApi: kubernetesApiRef, + }, + factory: ({ kubernetesApi }) => + new KubernetesProxyClient({ + kubernetesApi, + }), + }), + }, }); -export const kubernetesAuthProvidersApi = createApiExtension({ - factory: createApiFactory({ - api: kubernetesAuthProvidersApiRef, - deps: { - gitlabAuthApi: gitlabAuthApiRef, - googleAuthApi: googleAuthApiRef, - microsoftAuthApi: microsoftAuthApiRef, - oktaAuthApi: oktaAuthApiRef, - oneloginAuthApi: oneloginAuthApiRef, - }, - factory: ({ - gitlabAuthApi, - googleAuthApi, - microsoftAuthApi, - oktaAuthApi, - oneloginAuthApi, - }) => { - const oidcProviders = { - gitlab: gitlabAuthApi, - google: googleAuthApi, - microsoft: microsoftAuthApi, - okta: oktaAuthApi, - onelogin: oneloginAuthApi, - }; - - return new KubernetesAuthProviders({ - microsoftAuthApi, +export const kubernetesAuthProvidersApi = ApiBlueprint.make({ + name: 'auth-providers', + params: { + factory: createApiFactory({ + api: kubernetesAuthProvidersApiRef, + deps: { + gitlabAuthApi: gitlabAuthApiRef, + googleAuthApi: googleAuthApiRef, + microsoftAuthApi: microsoftAuthApiRef, + oktaAuthApi: oktaAuthApiRef, + oneloginAuthApi: oneloginAuthApiRef, + }, + factory: ({ + gitlabAuthApi, googleAuthApi, - oidcProviders, - }); - }, - }), + microsoftAuthApi, + oktaAuthApi, + oneloginAuthApi, + }) => { + const oidcProviders = { + gitlab: gitlabAuthApi, + google: googleAuthApi, + microsoft: microsoftAuthApi, + okta: oktaAuthApi, + onelogin: oneloginAuthApi, + }; + + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders, + }); + }, + }), + }, }); -export const kubernetesClusterLinkFormatterApi = createApiExtension({ - factory: createApiFactory({ - api: kubernetesClusterLinkFormatterApiRef, - deps: { googleAuthApi: googleAuthApiRef }, - factory: deps => { - const formatters = getDefaultFormatters(deps); - return new KubernetesClusterLinkFormatter({ - formatters, - defaultFormatterName: DEFAULT_FORMATTER_NAME, - }); - }, - }), +export const kubernetesClusterLinkFormatterApi = ApiBlueprint.make({ + name: 'cluster-link-formatter', + params: { + factory: createApiFactory({ + api: kubernetesClusterLinkFormatterApiRef, + deps: { googleAuthApi: googleAuthApiRef }, + factory: deps => { + const formatters = getDefaultFormatters(deps); + return new KubernetesClusterLinkFormatter({ + formatters, + defaultFormatterName: DEFAULT_FORMATTER_NAME, + }); + }, + }), + }, }); diff --git a/plugins/kubernetes/src/alpha/entityContents.tsx b/plugins/kubernetes/src/alpha/entityContents.tsx index 9ad0b6843f..de11310981 100644 --- a/plugins/kubernetes/src/alpha/entityContents.tsx +++ b/plugins/kubernetes/src/alpha/entityContents.tsx @@ -19,16 +19,18 @@ import { compatWrapper, convertLegacyRouteRef, } from '@backstage/core-compat-api'; -import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; import { rootCatalogKubernetesRouteRef } from '../plugin'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; -export const entityKubernetesContent = createEntityContentExtension({ - defaultPath: 'kubernetes', - defaultTitle: 'Kubernetes', +export const entityKubernetesContent = EntityContentBlueprint.make({ name: 'kubernetes', - routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), - loader: () => - import('./KubernetesContentPage').then(m => - compatWrapper(), - ), + params: { + defaultPath: 'kubernetes', + defaultTitle: 'Kubernetes', + routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), + loader: () => + import('./KubernetesContentPage').then(m => + compatWrapper(), + ), + }, }); diff --git a/plugins/kubernetes/src/alpha/pages.tsx b/plugins/kubernetes/src/alpha/pages.tsx index 8b5db150bd..a58ffec94f 100644 --- a/plugins/kubernetes/src/alpha/pages.tsx +++ b/plugins/kubernetes/src/alpha/pages.tsx @@ -16,18 +16,20 @@ import React from 'react'; // Add this line to import React -import { createPageExtension } from '@backstage/frontend-plugin-api'; +import { PageBlueprint } from '@backstage/frontend-plugin-api'; import { compatWrapper, convertLegacyRouteRef, } from '@backstage/core-compat-api'; import { rootCatalogKubernetesRouteRef } from '../plugin'; -export const kubernetesPage = createPageExtension({ - defaultPath: '/kubernetes', - // you can reuse the existing routeRef - // by wrapping into the convertLegacyRouteRef. - routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), - // these inputs usually match the props required by the component. - loader: () => import('../Router').then(m => compatWrapper()), +export const kubernetesPage = PageBlueprint.make({ + params: { + defaultPath: '/kubernetes', + // you can reuse the existing routeRef + // by wrapping into the convertLegacyRouteRef. + routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), + // these inputs usually match the props required by the component. + loader: () => import('../Router').then(m => compatWrapper()), + }, }); diff --git a/plugins/kubernetes/src/alpha/plugin.tsx b/plugins/kubernetes/src/alpha/plugin.tsx index 47433bce35..1369133a24 100644 --- a/plugins/kubernetes/src/alpha/plugin.tsx +++ b/plugins/kubernetes/src/alpha/plugin.tsx @@ -15,7 +15,7 @@ */ import { convertLegacyRouteRefs } from '@backstage/core-compat-api'; -import { createPlugin } from '@backstage/frontend-plugin-api'; +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; import { kubernetesPage } from './pages'; import { entityKubernetesContent } from './entityContents'; import { rootCatalogKubernetesRouteRef } from '../plugin'; @@ -26,7 +26,7 @@ import { kubernetesProxyApi, } from './apis'; -export default createPlugin({ +export default createFrontendPlugin({ id: 'kubernetes', extensions: [ kubernetesPage, diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 1a9e5ad4bc..99dcdf4f13 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-notifications-backend-module-email +## 0.2.0-next.3 + +### Patch Changes + +- 83faf24: Notification email processor supports allowing or denying specific email addresses from receiving notifications +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + +## 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..bfe3467dd1 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.3", "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..a876a548d2 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}`); } @@ -196,15 +207,36 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private async getRecipientEmails( notification: Notification, options: NotificationSendOptions, - ) { - if (options.recipients.type === 'broadcast' || notification.user === null) { - return await this.getBroadcastEmails(); + ): Promise { + let emails: string[]; + if (options.recipients.type === 'broadcast') { + emails = await this.getBroadcastEmails(); + } else if (options.recipients.type === 'entity' && !!notification.user) { + emails = await this.getUserEmail(notification.user); + } else { + this.logger.info( + `Unknown notification type ${options.recipients.type} or missing user.`, + ); + return []; } - 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}`); @@ -311,6 +343,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { return; } + this.logger.debug(`Sending notification emails to: ${emails.join(',')}`); + if (!this.templateRenderer) { await this.sendPlainEmail(notification, emails); return; 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/packages/backend-app-api/src/services/implementations/cache/index.ts b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts similarity index 75% rename from packages/backend-app-api/src/services/implementations/cache/index.ts rename to plugins/notifications-backend-module-email/src/processor/transports/stream.ts index f96ee77182..26ed401366 100644 --- a/packages/backend-app-api/src/services/implementations/cache/index.ts +++ b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -13,5 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createTransport } from 'nodemailer'; -export { cacheServiceFactory } from './cacheServiceFactory'; +export const createStreamTransport = () => { + return createTransport({ streamTransport: true }); +}; diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 9afeff1a51..65496509f3 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-notifications-backend +## 0.3.4-next.3 + +### Patch Changes + +- 7a05f50: Allow using notifications without users in the catalog +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## 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 8bb3fd2acf..501cd50beb 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.3", "backstage": { "role": "backend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index c6df60c56e..d4e678c8f1 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -42,6 +42,7 @@ async function createStore(databaseId: TestDatabaseId) { const idOnly = (notification: Notification) => notification.id; const user = 'user:default/john.doe'; +const otherUser = 'user:default/jane.doe'; const id0 = '08e0871e-e60a-4f68-8110-5ae3513f992e'; const id1 = '01e0871e-e60a-4f68-8110-5ae3513f992e'; @@ -140,7 +141,7 @@ const testNotification7: Notification = { }; const otherUserNotification: Notification = { id: id0, - user: 'user:default/jane.doe', + user: otherUser, created: new Date(now), origin: 'plugin-test', payload: { @@ -246,6 +247,34 @@ describe.each(databases.eachSupportedId())( expect(notifications.map(idOnly)).toEqual([id2, id3, id1]); }); + it('should return correct broadcast notifications for different users', async () => { + await storage.saveNotification(testNotification1); + await storage.saveBroadcast(testNotification2); + await storage.saveNotification(testNotification3); + await storage.saveNotification(otherUserNotification); + + await storage.markRead({ ids: [id1, id2], user }); + + const notifications = await storage.getNotifications({ + user, + }); + expect(notifications.map(idOnly)).toEqual([id2, id3, id1]); + expect(notifications[1].user).toBe(user); + + let otherUserNotifications = await storage.getNotifications({ + user: otherUser, + }); + expect(otherUserNotifications.map(idOnly)).toEqual([id0, id2]); + expect(otherUserNotifications[1].user).toBeNull(); + + await storage.markRead({ ids: [id0, id2], user: otherUser }); + otherUserNotifications = await storage.getNotifications({ + user: otherUser, + }); + expect(otherUserNotifications.map(idOnly)).toEqual([id0, id2]); + expect(otherUserNotifications[1].user).toBe(otherUser); + }); + it('should allow searching for notifications', async () => { await storage.saveNotification(testNotification2); await storage.saveBroadcast(testNotification1); @@ -571,6 +600,23 @@ describe.each(databases.eachSupportedId())( const notification = await storage.getNotification({ id: id2 }); expect(notification?.id).toEqual(id2); }); + + it('should consider user for broadcast by id', async () => { + await storage.saveBroadcast(testNotification1); + + let notification = await storage.getNotification({ id: id1, user }); + expect(notification?.id).toEqual(id1); + expect(notification?.user).toBeNull(); + await storage.markRead({ ids: [id1], user }); + notification = await storage.getNotification({ id: id1, user }); + expect(notification?.user).toBe(user); + + const otherNotification = await storage.getNotification({ + id: id1, + user: otherUser, + }); + expect(otherNotification?.user).toBeNull(); + }); }); describe('markRead', () => { diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index 3d027128ec..b23f5254f4 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -138,14 +138,14 @@ export class DatabaseNotificationsStore implements NotificationsStore { }; }; - private getBroadcastUnion = () => { + private getBroadcastUnion = (user?: string | null) => { return this.db('broadcast') - .leftJoin( - 'broadcast_user_status', - 'id', - '=', - 'broadcast_user_status.broadcast_id', - ) + .leftJoin('broadcast_user_status', function clause() { + const join = this.on('id', '=', 'broadcast_user_status.broadcast_id'); + if (user !== null && user !== undefined) { + join.andOnVal('user', '=', user); + } + }) .select(NOTIFICATION_COLUMNS); }; @@ -156,7 +156,7 @@ export class DatabaseNotificationsStore implements NotificationsStore { const subQuery = this.db('notification') .select(NOTIFICATION_COLUMNS) - .unionAll([this.getBroadcastUnion()]) + .unionAll([this.getBroadcastUnion(user)]) .as('notifications'); const query = this.db.from(subQuery).where(q => { @@ -345,16 +345,19 @@ export class DatabaseNotificationsStore implements NotificationsStore { broadcastQuery.update({ ...updateColumns, read: undefined }), ]); - return await this.getNotification({ id }); + return await this.getNotification({ id, user: notification.user }); } - async getNotification(options: { id: string }): Promise { + async getNotification(options: { + id: string; + user?: string | null; + }): Promise { const rows = await this.db .select('*') .from( this.db('notification') .select(NOTIFICATION_COLUMNS) - .unionAll([this.getBroadcastUnion()]) + .unionAll([this.getBroadcastUnion(options.user)]) .as('notifications'), ) .where('id', options.id) 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..9ced24709b --- /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)].filter(Boolean); +}; 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..c9b17e9fc3 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-notifications-node +## 0.2.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## 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 d71fb7af82..4eed1f410c 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.3", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 42c55615b2..a9c6c00929 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-notifications +## 0.3.0-next.1 + +### Minor Changes + +- 0410fc9: By default, set notification as read when opening snackbar or web notification link + +### Patch Changes + +- 80b84f7: Fixed issue with notification reloading on page change +- Updated dependencies + - @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-notifications-common@0.0.5 + - @backstage/plugin-signals-react@0.0.4 + ## 0.2.4-next.0 ### Patch Changes 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/package.json b/plugins/notifications/package.json index 5dbfd83255..01d49fb8e5 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.2.4-next.0", + "version": "0.3.0-next.1", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 7b55c63747..22ce11db69 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -111,10 +111,12 @@ export const NotificationsSidebarItem = (props?: { const notificationsApi = useApi(notificationsApiRef); const alertApi = useApi(alertApiRef); const [unreadCount, setUnreadCount] = React.useState(0); - const notificationsRoute = useRouteRef(rootRouteRef); + 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(); @@ -124,8 +126,21 @@ export const NotificationsSidebarItem = (props?: { <> { + 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); }} > @@ -174,7 +189,6 @@ export const NotificationsSidebarItem = (props?: { ) { return; } - notificationsApi .getNotification(signal.notification_id) .then(notification => { @@ -196,6 +210,7 @@ export const NotificationsSidebarItem = (props?: { ? `${notification.payload.title.substring(0, 50)}...` : notification.payload.title; enqueueSnackbar(snackBarText, { + key: notification.id, variant: notification.payload.severity, anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, action, @@ -258,7 +273,10 @@ 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..cc69a0e815 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -13,19 +13,19 @@ * 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 { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { notificationsApiRef } from '../api'; import { rootRouteRef } from '../routes'; -import { useRouteRef } from '@backstage/core-plugin-api'; -import { useNavigate } from 'react-router-dom'; -/** @public */ +/** @internal */ export function useWebNotifications(enabled: boolean) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); - const notificationsRoute = useRouteRef(rootRouteRef); - const navigate = useNavigate(); + const notificationsRoute = useRouteRef(rootRouteRef)(); + const notificationsApi = useApi(notificationsApiRef); - useEffect(() => { + const requestUserPermission = useCallback(() => { if ( enabled && 'Notification' in window && @@ -57,16 +57,20 @@ 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()); + window.open(notificationsRoute); } notification.close(); }; return notification; }, - [webNotificationPermission, navigate, notificationsRoute], + [webNotificationPermission, notificationsApi, 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..dc0d303344 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-org-react +## 0.1.27-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 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..d3986a698b 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.3", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 69d6b3fc0f..bc512b3177 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-org +## 0.6.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog-common@1.0.26-next.2 + +## 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..d96825c601 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -4,13 +4,143 @@ ```ts import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; // @alpha (undocumented) const _default: BackstagePlugin< {}, { - catalogIndex: ExternalRouteRef; + catalogIndex: ExternalRouteRef; + }, + { + 'entity-card:org/group-profile': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'group-profile' + >; + 'entity-card:org/members-list': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'members-list' + >; + 'entity-card:org/ownership': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'ownership' + >; + 'entity-card:org/user-profile': ExtensionDefinition< + { + filter: string | undefined; + }, + { + filter?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-card', + undefined, + 'user-profile' + >; } >; export default _default; diff --git a/plugins/org/package.json b/plugins/org/package.json index 72ea35013d..3190177622 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.3", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/org/src/alpha.tsx b/plugins/org/src/alpha.tsx index 3fbf5bcc41..61054a2d8a 100644 --- a/plugins/org/src/alpha.tsx +++ b/plugins/org/src/alpha.tsx @@ -18,53 +18,61 @@ import { compatWrapper, convertLegacyRouteRefs, } from '@backstage/core-compat-api'; -import { createPlugin } from '@backstage/frontend-plugin-api'; +import { createFrontendPlugin } from '@backstage/frontend-plugin-api'; import React from 'react'; import { catalogIndexRouteRef } from './routes'; -import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha'; +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; /** @alpha */ -const EntityGroupProfileCard = createEntityCardExtension({ +const EntityGroupProfileCard = EntityCardBlueprint.make({ name: 'group-profile', - filter: 'kind:group', - loader: async () => - import('./components/Cards/Group/GroupProfile/GroupProfileCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:group', + loader: async () => + import('./components/Cards/Group/GroupProfile/GroupProfileCard').then(m => + compatWrapper(), + ), + }, }); /** @alpha */ -const EntityMembersListCard = createEntityCardExtension({ +const EntityMembersListCard = EntityCardBlueprint.make({ name: 'members-list', - filter: 'kind:group', - loader: async () => - import('./components/Cards/Group/MembersList/MembersListCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:group', + loader: async () => + import('./components/Cards/Group/MembersList/MembersListCard').then(m => + compatWrapper(), + ), + }, }); /** @alpha */ -const EntityOwnershipCard = createEntityCardExtension({ +const EntityOwnershipCard = EntityCardBlueprint.make({ name: 'ownership', - filter: 'kind:group,user', - loader: async () => - import('./components/Cards/OwnershipCard/OwnershipCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:group,user', + loader: async () => + import('./components/Cards/OwnershipCard/OwnershipCard').then(m => + compatWrapper(), + ), + }, }); /** @alpha */ -const EntityUserProfileCard = createEntityCardExtension({ +const EntityUserProfileCard = EntityCardBlueprint.make({ name: 'user-profile', - filter: 'kind:user', - loader: async () => - import('./components/Cards/User/UserProfileCard/UserProfileCard').then(m => - compatWrapper(), - ), + params: { + filter: 'kind:user', + loader: async () => + import('./components/Cards/User/UserProfileCard/UserProfileCard').then( + m => compatWrapper(), + ), + }, }); /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'org', extensions: [ EntityGroupProfileCard, diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx index db54be9e02..e5bfa053c0 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx @@ -15,11 +15,14 @@ */ import { Entity, GroupEntity } from '@backstage/catalog-model'; -import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; -import { TestApiProvider } from '@backstage/test-utils'; +import { + catalogApiRef, + EntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import Grid from '@material-ui/core/Grid'; -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import React, { ComponentType, PropsWithChildren } from 'react'; import { groupA, mockedCatalogApiSupportingGroups, @@ -29,6 +32,15 @@ import { MembersListCard } from './MembersListCard'; export default { title: 'Plugins/Org/Group Members List Card', component: MembersListCard, + decorators: [ + (Story: ComponentType>) => + wrapInTestApp( +
    + +
    , + { mountedRoutes: { '/': entityRouteRef } }, + ), + ], }; const makeUser = ({ @@ -108,45 +120,38 @@ const bob = makeUser({ const catalogApi = (items: Entity[]) => ({ getEntities: () => Promise.resolve({ items }), }); - export const Default = () => ( - - - - - - - + + + + + - - - + + + ); export const Empty = () => ( - - - - - - - + + + + + - - - + + + ); export const AggregateMembersToggle = () => ( - - - - - - - + + + + + - - - + + + ); diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 559697105d..e41e5ece35 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.1.20-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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..8ea05a53dc 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.3", "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..7c39002f3d 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-permission-backend +## 0.5.47-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + +## 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 a5b6a9fb59..1a80b25571 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.3", "backstage": { "role": "backend-plugin", "pluginId": "permission", 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/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-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 3e29fe0c2a..8fe3011f5c 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-permission-node +## 0.8.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 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/package.json b/plugins/permission-node/package.json index 4e024a3f10..a8d49bea69 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.3", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", 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..9bb3d05258 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-proxy-backend +## 0.5.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 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 655ccfed9d..383a8012ca 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.3", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", diff --git a/plugins/proxy-backend/src/service/router.credentials.test.ts b/plugins/proxy-backend/src/service/router.credentials.test.ts index ac32179ef3..a363e7c632 100644 --- a/plugins/proxy-backend/src/service/router.credentials.test.ts +++ b/plugins/proxy-backend/src/service/router.credentials.test.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { - authServiceFactory, - httpAuthServiceFactory, -} from '@backstage/backend-app-api'; +import { authServiceFactory } from '@backstage/backend-defaults/auth'; +import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth'; import { mockServices, registerMswTestHooks, diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index fe925ffb7d..2360347e41 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.1.15-next.3 + +### Patch Changes + +- 187f583: Added examples for publish:azure action and updated its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..afdb3bdd37 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.3", "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..34aa1dee75 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.1.13-next.3 + +### Patch Changes + +- d57967c: Add ability to set the initial commit message when initializing a repository using the scaffolder action. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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/api-report.md b/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md index a484f49f77..1f1a7ca1d0 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/api-report.md @@ -36,6 +36,7 @@ export function createPublishBitbucketCloudAction(options: { description?: string | undefined; defaultBranch?: string | undefined; repoVisibility?: 'private' | 'public' | undefined; + gitCommitMessage?: string | undefined; sourcePath?: string | undefined; token?: string | undefined; }, diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 0446c39b0c..aab9ffc772 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.3", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.ts index 99e8e52a43..71eacea955 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.ts @@ -111,6 +111,7 @@ export function createPublishBitbucketCloudAction(options: { description?: string; defaultBranch?: string; repoVisibility?: 'private' | 'public'; + gitCommitMessage?: string; sourcePath?: string; token?: string; }>({ @@ -141,6 +142,11 @@ export function createPublishBitbucketCloudAction(options: { type: 'string', description: `Sets the default branch on the repository. The default value is 'master'`, }, + gitCommitMessage: { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, + }, sourcePath: { title: 'Source Path', description: @@ -178,6 +184,7 @@ export function createPublishBitbucketCloudAction(options: { repoUrl, description, defaultBranch = 'master', + gitCommitMessage, repoVisibility = 'private', } = ctx.input; @@ -256,9 +263,9 @@ export function createPublishBitbucketCloudAction(options: { auth, defaultBranch, logger: ctx.logger, - commitMessage: config.getOptionalString( - 'scaffolder.defaultCommitMessage', - ), + commitMessage: + gitCommitMessage || + config.getOptionalString('scaffolder.defaultCommitMessage'), gitAuthorInfo, }); 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..53cd5df1e5 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.1.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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 0ce8ac90e8..ea7f9b6e44 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.3", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index 8ab0a80df0..dbc4f8ef02 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.2.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.3 + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.3 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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 e29e75ee12..df1bd06c4f 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.3", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 9c9b493c1d..dd42aa963e 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.24-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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/api-report.md b/plugins/scaffolder-backend-module-confluence-to-markdown/api-report.md index b56ade73a2..df2e74337f 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/api-report.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/api-report.md @@ -8,7 +8,7 @@ import { Config } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public const confluenceToMarkdownModule: BackendFeatureCompat; @@ -16,7 +16,7 @@ export default confluenceToMarkdownModule; // @public (undocumented) export const createConfluenceToMarkdownAction: (options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; config: Config; }) => TemplateAction< diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index a0d5631608..3910f5cd5d 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.3", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index 36ae750723..5cf8969b55 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; -import { UrlReader, loggerToWinstonLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { @@ -28,6 +28,7 @@ import { examples } from './confluenceToMarkdown.examples'; import yaml from 'yaml'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; @@ -51,7 +52,7 @@ describe('confluence:transform:markdown examples', () => { }), ); - let reader: UrlReader; + let reader: UrlReaderService; let mockContext: ActionContext<{ confluenceUrls: string[]; repoUrl: string; diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index 4c3c6b891f..a54e5dcc63 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -15,7 +15,7 @@ */ import { createConfluenceToMarkdownAction } from './confluenceToMarkdown'; -import { UrlReader, loggerToWinstonLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { @@ -27,6 +27,7 @@ import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; @@ -50,7 +51,7 @@ describe('confluence:transform:markdown', () => { }), ); - let reader: UrlReader; + let reader: UrlReaderService; let mockContext: ActionContext<{ confluenceUrls: string[]; repoUrl: string; diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts index e88f42e39c..aa9f94988a 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.ts @@ -15,7 +15,6 @@ */ import { Config } from '@backstage/config'; -import { UrlReader } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { createTemplateAction, @@ -34,13 +33,14 @@ import { getConfluenceConfig, } from './helpers'; import { examples } from './confluenceToMarkdown.examples'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; /** * @public */ export const createConfluenceToMarkdownAction = (options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; config: Config; }) => { diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 71ce038f43..f8e3b566e3 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.47-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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/api-report.md b/plugins/scaffolder-backend-module-cookiecutter/api-report.md index 2dbf462063..a2332da624 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/api-report.md +++ b/plugins/scaffolder-backend-module-cookiecutter/api-report.md @@ -10,7 +10,7 @@ import { ContainerRunner } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public const cookiecutterModule: BackendFeatureCompat; @@ -18,7 +18,7 @@ export default cookiecutterModule; // @public export function createFetchCookiecutterAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; containerRunner?: ContainerRunner; }): TemplateAction< diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index ec1899099b..f412ff11b1 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.3", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", @@ -44,6 +44,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts index ae46ef1bd6..73c17d8ac0 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader, ContainerRunner } from '@backstage/backend-common'; +import { ContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; @@ -25,6 +25,7 @@ import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './cookiecutter.examples'; import yaml from 'yaml'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); @@ -71,7 +72,7 @@ describe('fetch:cookiecutter', () => { runContainer: jest.fn(), }; - const mockReader: UrlReader = { + const mockReader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index fad544488a..52946ce7fd 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader, ContainerRunner } from '@backstage/backend-common'; +import { ContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; @@ -24,6 +24,7 @@ import { join } from 'path'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'stream'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; const executeShellCommand = jest.fn(); const commandExists = jest.fn(); @@ -70,7 +71,7 @@ describe('fetch:cookiecutter', () => { runContainer: jest.fn(), }; - const mockReader: UrlReader = { + const mockReader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index efbe0a07cf..6e540d7fca 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { ContainerRunner, UrlReader } from '@backstage/backend-common'; -import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; +import { ContainerRunner } from '@backstage/backend-common'; +import { + UrlReaderService, + resolveSafeChildPath, +} from '@backstage/backend-plugin-api'; import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; @@ -140,7 +143,7 @@ export class CookiecutterRunner { * @public */ export function createFetchCookiecutterAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; containerRunner?: ContainerRunner; }) { diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md index 551141df6c..dac03aa7f5 100644 --- a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-backend-module-gcp +## 0.1.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..a8035eb2ff 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.3", "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..e4c05d440f 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.1.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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 f39e67cedc..9221c6893f 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.3", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index 983d3cebb0..45a93e76a9 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.1.13-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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 14b0305b10..2e9f648336 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.3", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index d39143c7c4..0d5924a780 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.4.1-next.3 + +### Patch Changes + +- 6d4cb97: Added examples for github:repo:create action and improved test cases +- cd203f1: Added examples for action github:pages and improved its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..de6fb2d2a0 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.3", "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..417f6ffca7 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.4.5-next.3 + +### Patch Changes + +- da97131: Added test cases for gitlab:issues:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..37eaf5b7ac 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.3", "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..00d3cfa3eb 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.0.6-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.3 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..dd35dc9864 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.3", "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..d01f0ff03e 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.40-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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/api-report.md b/plugins/scaffolder-backend-module-rails/api-report.md index 9b663d33d6..31e8f6834c 100644 --- a/plugins/scaffolder-backend-module-rails/api-report.md +++ b/plugins/scaffolder-backend-module-rails/api-report.md @@ -8,11 +8,11 @@ import { ContainerRunner } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public export function createFetchRailsAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; containerRunner?: ContainerRunner; allowedImageNames?: string[]; diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 38088ccfc8..7f7d570962 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.3", "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-rails/src/actions/fetch/rails/index.examples.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts index 37e678ab6a..6bc84e04ad 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts @@ -27,7 +27,7 @@ jest.mock('./railsNewRunner', () => { }; }); -import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { ContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { resolve as resolvePath } from 'path'; @@ -37,6 +37,7 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './index.examples'; import yaml from 'yaml'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); @@ -66,7 +67,7 @@ describe('fetch:rails', () => { workspacePath: mockDir.path, }); - const mockReader: UrlReader = { + const mockReader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 40514dc7c5..616c90e0de 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -27,7 +27,7 @@ jest.mock('./railsNewRunner', () => { }; }); -import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { ContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { resolve as resolvePath } from 'path'; @@ -36,6 +36,7 @@ import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createMockDirectory } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { Writable } from 'stream'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('fetch:rails', () => { const mockDir = createMockDirectory(); @@ -65,7 +66,7 @@ describe('fetch:rails', () => { workspacePath: mockDir.path, }); - const mockReader: UrlReader = { + const mockReader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 94a2a3539b..4d5db0f3ef 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { ContainerRunner } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; @@ -28,6 +28,7 @@ import { resolve as resolvePath } from 'path'; import { RailsNewRunner } from './railsNewRunner'; import { PassThrough } from 'stream'; import { examples } from './index.examples'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; /** * Creates the `fetch:rails` Scaffolder action. @@ -40,7 +41,7 @@ import { examples } from './index.examples'; * @public */ export function createFetchRailsAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; containerRunner?: ContainerRunner; /** A list of image names that are allowed to be passed as imageName input */ diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 7515c6494b..fe19c66f69 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.31-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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 81171ea78d..f919dd5fcb 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.3", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", 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..d760efd3ef 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.3.7-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.3 + +## 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..b766612f0d 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.3", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b1bb14c6a7..5b4fdade86 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,68 @@ # @backstage/plugin-scaffolder-backend +## 1.23.1-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.3 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.3 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.3 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.3 + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.3 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.3 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.3 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.3 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..207b54e63f 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -63,7 +63,7 @@ import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; import { ZodType } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -95,7 +95,7 @@ export interface CreateBuiltInActionsOptions { catalogClient: CatalogApi; config: Config; integrations: ScmIntegrations; - reader: UrlReader; + reader: UrlReaderService; } // @public @@ -154,7 +154,7 @@ export function createFetchCatalogEntityAction(options: { // @public export function createFetchPlainAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; }): TemplateAction_2< { @@ -167,7 +167,7 @@ export function createFetchPlainAction(options: { // @public export function createFetchPlainFileAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; }): TemplateAction_2< { @@ -180,7 +180,7 @@ export function createFetchPlainFileAction(options: { // @public export function createFetchTemplateAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; @@ -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) @@ -507,7 +507,7 @@ export interface RouterOptions { // (undocumented) permissions?: PermissionsService; // (undocumented) - reader: UrlReader; + reader: UrlReaderService; // (undocumented) scheduler?: PluginTaskScheduler; // (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..459111c835 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.3", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", @@ -62,6 +62,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/catalog-client": "workspace:^", @@ -86,6 +87,7 @@ "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", + "@opentelemetry/api": "^1.3.0", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", "concat-stream": "^2.0.0", @@ -115,6 +117,7 @@ }, "devDependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 673776bec9..dba867c96b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { @@ -82,7 +81,7 @@ import { } from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { createPublishGiteaAction } from '@backstage/plugin-scaffolder-backend-module-gitea'; -import { AuthService } from '@backstage/backend-plugin-api'; +import { AuthService, UrlReaderService } from '@backstage/backend-plugin-api'; /** * The options passed to {@link createBuiltinActions} @@ -90,9 +89,9 @@ import { AuthService } from '@backstage/backend-plugin-api'; */ export interface CreateBuiltInActionsOptions { /** - * The {@link @backstage/backend-common#UrlReader} interface that will be used in the default actions. + * The {@link @backstage/backend-plugin-api#UrlReaderService} interface that will be used in the default actions. */ - reader: UrlReader; + reader: UrlReaderService; /** * The {@link @backstage/integrations#ScmIntegrations} that will be used in the default actions. */ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts index 82a32fcbac..9dbb1f9a03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts @@ -17,13 +17,13 @@ import yaml from 'yaml'; import { resolve as resolvePath } from 'path'; -import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainAction } from './plain'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { examples } from './plain.examples'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; jest.mock('@backstage/plugin-scaffolder-node', () => ({ ...jest.requireActual('@backstage/plugin-scaffolder-node'), @@ -38,7 +38,7 @@ describe('fetch:plain examples', () => { }, }), ); - const reader: UrlReader = { + const reader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts index bc20fafe19..8a52ef7392 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.test.ts @@ -21,11 +21,11 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { resolve as resolvePath } from 'path'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; import { createFetchPlainAction } from './plain'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('fetch:plain', () => { const integrations = ScmIntegrations.fromConfig( @@ -35,7 +35,7 @@ describe('fetch:plain', () => { }, }), ); - const reader: UrlReader = { + const reader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts index c6beaa2d67..a4f795dd54 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -14,8 +14,10 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; -import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; +import { + resolveSafeChildPath, + UrlReaderService, +} from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; import { examples } from './plain.examples'; @@ -32,7 +34,7 @@ export const ACTION_ID = 'fetch:plain'; * @public */ export function createFetchPlainAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; }) { const { reader, integrations } = options; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts index 9978d6d8f5..a091a6ecfe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts @@ -23,12 +23,12 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import yaml from 'yaml'; import { resolve as resolvePath } from 'path'; -import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchPlainFileAction } from './plainFile'; import { fetchFile } from '@backstage/plugin-scaffolder-node'; import { examples } from './plainFile.examples'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('fetch:plain:file examples', () => { const integrations = ScmIntegrations.fromConfig( @@ -38,7 +38,7 @@ describe('fetch:plain:file examples', () => { }, }), ); - const reader: UrlReader = { + const reader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts index bf40c7ad0e..637a209bd9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts @@ -21,11 +21,11 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { resolve as resolvePath } from 'path'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { UrlReader } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchFile } from '@backstage/plugin-scaffolder-node'; import { createFetchPlainFileAction } from './plainFile'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; describe('fetch:plain:file', () => { const integrations = ScmIntegrations.fromConfig( @@ -35,7 +35,7 @@ describe('fetch:plain:file', () => { }, }), ); - const reader: UrlReader = { + const reader: UrlReaderService = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts index 92db7aa3ed..a07c9bfc6b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; import { examples } from './plainFile.examples'; @@ -29,7 +29,7 @@ import { * @public */ export function createFetchPlainFileAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; }) { const { reader, integrations } = options; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts index 02fe0c30a4..41ce875317 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -16,7 +16,7 @@ import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { resolvePackagePath } from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -71,7 +71,7 @@ describe('fetch:template examples', () => { beforeEach(() => { mockDir.clear(); action = createFetchTemplateAction({ - reader: Symbol('UrlReader') as unknown as UrlReader, + reader: Symbol('UrlReader') as unknown as UrlReaderService, integrations: Symbol('Integrations') as unknown as ScmIntegrations, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 0542b6a50d..61918e8450 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -21,8 +21,10 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import { UrlReader } from '@backstage/backend-common'; -import { resolvePackagePath } from '@backstage/backend-plugin-api'; +import { + UrlReaderService, + resolvePackagePath, +} from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; import { createFetchTemplateAction } from './template'; import { @@ -78,7 +80,7 @@ describe('fetch:template', () => { workspace: {}, }); action = createFetchTemplateAction({ - reader: Symbol('UrlReader') as unknown as UrlReader, + reader: Symbol('UrlReader') as unknown as UrlReaderService, integrations: Symbol('Integrations') as unknown as ScmIntegrations, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 0983199339..2025202c22 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -15,7 +15,7 @@ */ import { extname } from 'path'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; @@ -40,7 +40,7 @@ import { examples } from './template.examples'; * @public */ export function createFetchTemplateAction(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; 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/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 3b88fd0f10..3d7d8b8b1d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -26,6 +26,7 @@ import { PassThrough } from 'stream'; import { generateExampleOutput, isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; import { TemplateActionRegistry } from '../actions'; +import { metrics } from '@opentelemetry/api'; import { SecureTemplater, SecureTemplateRenderer, @@ -520,36 +521,62 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } function scaffoldingTracker() { - const taskCount = createCounterMetric({ + // prom-client metrics are deprecated in favour of OpenTelemetry metrics. + const promTaskCount = createCounterMetric({ name: 'scaffolder_task_count', help: 'Count of task runs', labelNames: ['template', 'user', 'result'], }); - const taskDuration = createHistogramMetric({ + const promTaskDuration = createHistogramMetric({ name: 'scaffolder_task_duration', help: 'Duration of a task run', labelNames: ['template', 'result'], }); - const stepCount = createCounterMetric({ + const promtStepCount = createCounterMetric({ name: 'scaffolder_step_count', help: 'Count of step runs', labelNames: ['template', 'step', 'result'], }); - const stepDuration = createHistogramMetric({ + const promStepDuration = createHistogramMetric({ name: 'scaffolder_step_duration', help: 'Duration of a step runs', labelNames: ['template', 'step', 'result'], }); + const meter = metrics.getMeter('default'); + const taskCount = meter.createCounter('scaffolder.task.count', { + description: 'Count of task runs', + }); + + const taskDuration = meter.createHistogram('scaffolder.task.duration', { + description: 'Duration of a task run', + unit: 'seconds', + }); + + const stepCount = meter.createCounter('scaffolder.step.count', { + description: 'Count of step runs', + }); + + const stepDuration = meter.createHistogram('scaffolder.step.duration', { + description: 'Duration of a step runs', + unit: 'seconds', + }); + async function taskStart(task: TaskContext) { await task.emitLog(`Starting up task with ${task.spec.steps.length} steps`); const template = task.spec.templateInfo?.entityRef || ''; const user = task.spec.user?.ref || ''; - const taskTimer = taskDuration.startTimer({ + const startTime = process.hrtime(); + const taskTimer = promTaskDuration.startTimer({ template, }); + function endTime() { + const delta = process.hrtime(startTime); + return delta[0] + delta[1] / 1e9; + } + async function skipDryRun( step: TaskStep, action: TemplateAction, @@ -561,12 +588,17 @@ function scaffoldingTracker() { } async function markSuccessful() { - taskCount.inc({ + promTaskCount.inc({ template, user, result: 'ok', }); taskTimer({ result: 'ok' }); + + taskCount.add(1, { template, user, result: 'ok' }); + taskDuration.record(endTime(), { + result: 'ok', + }); } async function markFailed(step: TaskStep, err: Error) { @@ -574,12 +606,17 @@ function scaffoldingTracker() { stepId: step.id, status: 'failed', }); - taskCount.inc({ + promTaskCount.inc({ template, user, result: 'failed', }); taskTimer({ result: 'failed' }); + + taskCount.add(1, { template, user, result: 'failed' }); + taskDuration.record(endTime(), { + result: 'failed', + }); } async function markCancelled(step: TaskStep) { @@ -587,12 +624,17 @@ function scaffoldingTracker() { stepId: step.id, status: 'cancelled', }); - taskCount.inc({ + promTaskCount.inc({ template, user, result: 'cancelled', }); taskTimer({ result: 'cancelled' }); + + taskCount.add(1, { template, user, result: 'cancelled' }); + taskDuration.record(endTime(), { + result: 'cancelled', + }); } return { @@ -610,40 +652,61 @@ function scaffoldingTracker() { }); const template = task.spec.templateInfo?.entityRef || ''; - const stepTimer = stepDuration.startTimer({ + const startTime = process.hrtime(); + const stepTimer = promStepDuration.startTimer({ template, step: step.name, }); + function endTime() { + const delta = process.hrtime(startTime); + return delta[0] + delta[1] / 1e9; + } + async function markSuccessful() { await task.emitLog(`Finished step ${step.name}`, { stepId: step.id, status: 'completed', }); - stepCount.inc({ + promtStepCount.inc({ template, step: step.name, result: 'ok', }); stepTimer({ result: 'ok' }); + + stepCount.add(1, { template, step: step.name, result: 'ok' }); + stepDuration.record(endTime(), { + result: 'ok', + }); } async function markCancelled() { - stepCount.inc({ + promtStepCount.inc({ template, step: step.name, result: 'cancelled', }); stepTimer({ result: 'cancelled' }); + + stepCount.add(1, { template, step: step.name, result: 'cancelled' }); + stepDuration.record(endTime(), { + result: 'cancelled', + }); } async function markFailed() { - stepCount.inc({ + promtStepCount.inc({ template, step: step.name, result: 'failed', }); stepTimer({ result: 'failed' }); + + stepCount.add(1, { template, step: step.name, result: 'failed' }); + stepDuration.record(endTime(), { + result: 'failed', + }); } async function skipFalsy() { @@ -652,6 +715,11 @@ function scaffoldingTracker() { { stepId: step.id, status: 'skipped' }, ); stepTimer({ result: 'skipped' }); + + stepCount.add(1, { template, step: step.name, result: 'skipped' }); + stepDuration.record(endTime(), { + result: 'skipped', + }); } return { 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..51e11d630f 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -18,7 +18,6 @@ import { DatabaseManager, loggerToWinstonLogger, PluginDatabaseManager, - UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; @@ -47,7 +46,8 @@ import { } from '@backstage/plugin-permission-common'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; -import { MiddlewareFactory } from '@backstage/backend-app-api'; +import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; +import { UrlReaders } from '@backstage/backend-defaults/urlReader'; const mockAccess = jest.fn(); @@ -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..7dcf702bf7 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,10 +15,9 @@ */ import { + createLegacyAuthAdapters, HostDiscovery, PluginDatabaseManager, - UrlReader, - createLegacyAuthAdapters, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; @@ -35,22 +34,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 +57,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'; @@ -87,6 +87,7 @@ import { HttpAuthService, LifecycleService, PermissionsService, + UrlReaderService, } from '@backstage/backend-plugin-api'; import { IdentityApi, @@ -143,7 +144,7 @@ function isActionPermissionRuleInput( export interface RouterOptions { logger: Logger; config: Config; - reader: UrlReader; + reader: UrlReaderService; lifecycle?: LifecycleService; database: PluginDatabaseManager; catalogClient: CatalogApi; @@ -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..15a85c8aae 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-scaffolder-common +## 1.5.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 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..5f54e29d8e 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.2", "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..305f6073c5 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.1.10-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-test-utils@0.4.5-next.3 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.3 + +## 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..b5cef2fe7d 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.3", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 3dc6b6b952..5dd3145be7 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-scaffolder-node +## 0.4.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + +## 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..8b38008eee 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -16,7 +16,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'stream'; import { z } from 'zod'; @@ -196,7 +196,7 @@ export type ExecuteShellCommandOptions = { // @public export function fetchContents(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; baseUrl?: string; fetchUrl?: string; @@ -206,7 +206,7 @@ export function fetchContents(options: { // @public export function fetchFile(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; baseUrl?: string; fetchUrl?: string; @@ -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..375612b05c 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.3", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", diff --git a/plugins/scaffolder-node/src/actions/fetch.test.ts b/plugins/scaffolder-node/src/actions/fetch.test.ts index ea7f458fde..f831d20f35 100644 --- a/plugins/scaffolder-node/src/actions/fetch.test.ts +++ b/plugins/scaffolder-node/src/actions/fetch.test.ts @@ -18,7 +18,7 @@ jest.mock('fs-extra'); import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents, fetchFile } from './fetch'; @@ -39,7 +39,7 @@ describe('fetchContents helper', () => { const readUrl = jest.fn(); const readTree = jest.fn(); - const reader: UrlReader = { + const reader: UrlReaderService = { readUrl, readTree, search: jest.fn(), diff --git a/plugins/scaffolder-node/src/actions/fetch.ts b/plugins/scaffolder-node/src/actions/fetch.ts index 63bc4b1352..a0e8dd9f85 100644 --- a/plugins/scaffolder-node/src/actions/fetch.ts +++ b/plugins/scaffolder-node/src/actions/fetch.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; @@ -28,7 +28,7 @@ import path from 'path'; * @public */ export async function fetchContents(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; baseUrl?: string; fetchUrl?: string; @@ -67,7 +67,7 @@ export async function fetchContents(options: { * @public */ export async function fetchFile(options: { - reader: UrlReader; + reader: UrlReaderService; integrations: ScmIntegrations; baseUrl?: string; fetchUrl?: string; 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..93706dbd45 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-scaffolder-react +## 1.11.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/catalog-client@1.6.6-next.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-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.2 + +## 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..634d85eccf 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.3", "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/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" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import type { FormProps as FormProps_2 } from '@rjsf/core'; import { FormProps as FormProps_3 } from '@backstage/plugin-scaffolder-react'; +import { IconComponent } from '@backstage/core-plugin-api'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; import { PathParams } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; @@ -30,14 +35,64 @@ const _default: BackstagePlugin< edit: SubRouteRef; }, { - registerComponent: ExternalRouteRef; - viewTechDoc: ExternalRouteRef< + registerComponent: ExternalRouteRef; + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, + { + 'api:scaffolder': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + undefined + >; + 'page:scaffolder': ExtensionDefinition< { - name: string; - kind: string; - namespace: string; + path: string | undefined; }, - true + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + undefined + >; + 'nav-item:scaffolder': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined >; } >; 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..76b6aa2d93 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.3", "description": "The Backstage plugin that helps you create new things", "backstage": { "role": "frontend-plugin", diff --git a/plugins/scaffolder/src/alpha.tsx b/plugins/scaffolder/src/alpha.tsx index 37e2978802..3136626f47 100644 --- a/plugins/scaffolder/src/alpha.tsx +++ b/plugins/scaffolder/src/alpha.tsx @@ -16,14 +16,14 @@ import React from 'react'; import { - createApiExtension, createApiFactory, - createNavItemExtension, - createPageExtension, - createPlugin, + createFrontendPlugin, discoveryApiRef, fetchApiRef, identityApiRef, + ApiBlueprint, + PageBlueprint, + NavItemBlueprint, } from '@backstage/frontend-plugin-api'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import { @@ -51,40 +51,46 @@ export { type TemplateWizardPageProps, } from './next'; -const scaffolderApi = createApiExtension({ - factory: createApiFactory({ - api: scaffolderApiRef, - deps: { - discoveryApi: discoveryApiRef, - scmIntegrationsApi: scmIntegrationsApiRef, - fetchApi: fetchApiRef, - identityApi: identityApiRef, - }, - factory: ({ discoveryApi, scmIntegrationsApi, fetchApi, identityApi }) => - new ScaffolderClient({ - discoveryApi, - scmIntegrationsApi, - fetchApi, - identityApi, - }), - }), +const scaffolderApi = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: scaffolderApiRef, + deps: { + discoveryApi: discoveryApiRef, + scmIntegrationsApi: scmIntegrationsApiRef, + fetchApi: fetchApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi, scmIntegrationsApi, fetchApi, identityApi }) => + new ScaffolderClient({ + discoveryApi, + scmIntegrationsApi, + fetchApi, + identityApi, + }), + }), + }, }); -const scaffolderPage = createPageExtension({ - routeRef: convertLegacyRouteRef(rootRouteRef), - defaultPath: '/create', - loader: () => - import('./components/Router').then(m => compatWrapper()), +const scaffolderPage = PageBlueprint.make({ + params: { + routeRef: convertLegacyRouteRef(rootRouteRef), + defaultPath: '/create', + loader: () => + import('./components/Router').then(m => compatWrapper()), + }, }); -const scaffolderNavItem = createNavItemExtension({ - routeRef: convertLegacyRouteRef(rootRouteRef), - title: 'Create...', - icon: CreateComponentIcon, +const scaffolderNavItem = NavItemBlueprint.make({ + params: { + routeRef: convertLegacyRouteRef(rootRouteRef), + title: 'Create...', + icon: CreateComponentIcon, + }, }); /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'scaffolder', routes: convertLegacyRouteRefs({ root: rootRouteRef, 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/MyGroupsPicker/MyGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx index 94261ce0e4..4e54ee99ad 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx @@ -19,7 +19,10 @@ import { render, waitFor } from '@testing-library/react'; import { CatalogApi } from '@backstage/catalog-client'; import { MyGroupsPicker } from './MyGroupsPicker'; import { TestApiProvider } from '@backstage/test-utils'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; import { ErrorApi, @@ -29,6 +32,7 @@ import { } from '@backstage/core-plugin-api'; import userEvent from '@testing-library/user-event'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; +import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; // Create a mock IdentityApi const mockIdentityApi: IdentityApi = { @@ -117,6 +121,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > @@ -189,6 +197,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > @@ -246,6 +258,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > @@ -306,6 +322,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx index 1e78fa45a1..dcb4be4c44 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useEffect } from 'react'; import { errorApiRef, identityApiRef, @@ -23,11 +23,19 @@ import { import TextField from '@material-ui/core/TextField'; import FormControl from '@material-ui/core/FormControl'; import { MyGroupsPickerProps, MyGroupsPickerSchema } from './schema'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import Autocomplete, { + createFilterOptions, +} from '@material-ui/lab/Autocomplete'; +import { + catalogApiRef, + EntityDisplayName, + entityPresentationApiRef, + EntityRefPresentationSnapshot, +} from '@backstage/plugin-catalog-react'; import { NotFoundError } from '@backstage/errors'; import useAsync from 'react-use/esm/useAsync'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { VirtualizedListbox } from '../VirtualizedListbox'; export { MyGroupsPickerSchema }; @@ -43,19 +51,14 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => { const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); - const [groups, setGroups] = useState< - { - label: string; - ref: string; - }[] - >([]); + const entityPresentationApi = useApi(entityPresentationApiRef); - useAsync(async () => { + const { value: groups, loading } = useAsync(async () => { const { userEntityRef } = await identityApi.getBackstageIdentity(); if (!userEntityRef) { errorApi.post(new NotFoundError('No user entity ref found')); - return; + return { catalogEntities: [], entityRefToPresentation: new Map() }; } const { items } = await catalogApi.getEntities({ @@ -65,24 +68,38 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => { }, }); - const groupValues = items - .filter((e): e is Entity => Boolean(e)) - .map(item => ({ - label: item.metadata.title ?? item.metadata.name, - ref: stringifyEntityRef(item), - })); + const entityRefToPresentation = new Map< + string, + EntityRefPresentationSnapshot + >( + await Promise.all( + items.map(async item => { + const presentation = await entityPresentationApi.forEntity(item) + .promise; + return [stringifyEntityRef(item), presentation] as [ + string, + EntityRefPresentationSnapshot, + ]; + }), + ), + ); - setGroups(groupValues); + return { catalogEntities: items, entityRefToPresentation }; }); - const updateChange = ( - _: React.ChangeEvent<{}>, - value: { label: string; ref: string } | null, - ) => { - onChange(value?.ref ?? ''); + const updateChange = (_: React.ChangeEvent<{}>, value: Entity | null) => { + onChange(value ? stringifyEntityRef(value) : ''); }; - const selectedEntity = groups?.find(e => e.ref === formData) || null; + const selectedEntity = + groups?.catalogEntities.find(e => stringifyEntityRef(e) === formData) || + null; + + useEffect(() => { + if (groups?.catalogEntities.length === 1 && !selectedEntity) { + onChange(stringifyEntityRef(groups.catalogEntities[0])); + } + }, [groups, onChange, selectedEntity]); return ( { error={rawErrors?.length > 0} > group.label} + getOptionLabel={option => + groups?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.primaryTitle! + } + autoSelect renderInput={params => ( { FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }} variant="outlined" required={required} + InputProps={params.InputProps} /> )} + renderOption={option => } + filterOptions={createFilterOptions({ + stringify: option => + groups?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.primaryTitle!, + })} + ListboxComponent={VirtualizedListbox} /> ); 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/components/fields/VirtualizedListbox.test.tsx b/plugins/scaffolder/src/components/fields/VirtualizedListbox.test.tsx new file mode 100644 index 0000000000..7e77826e32 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/VirtualizedListbox.test.tsx @@ -0,0 +1,277 @@ +/* + * 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 { VirtualizedListbox } from './VirtualizedListbox'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('', () => { + it('Should forward additional props to the outer div', async () => { + const { baseElement } = await renderInTestApp( + , + ); + + expect(baseElement.children[0]).toMatchInlineSnapshot(` +
    +
    +
    +
    +
    +
    +
    + `); + }); + it('Should render with no items', async () => { + const { baseElement } = await renderInTestApp(); + + expect(baseElement.children[0]).toMatchInlineSnapshot(` +
    +
    +
    +
    +
    +
    +
    + `); + }); + it('Should render with one item', async () => { + const { baseElement } = await renderInTestApp( + + Item 1 + , + ); + + expect(baseElement.children[0]).toMatchInlineSnapshot(` +
    +
    +
    +
    + + Item 1 + +
    +
    +
    +
    + `); + }); + it('Should render with 10 items', async () => { + const { baseElement } = await renderInTestApp( + + {[...new Array(10)].map((_, i) => ( + Item {i} + ))} + , + ); + + expect(baseElement.children[0]).toMatchInlineSnapshot(` +
    +
    +
    +
    + + Item + 0 + + + Item + 1 + + + Item + 2 + + + Item + 3 + + + Item + 4 + + + Item + 5 + + + Item + 6 + + + Item + 7 + + + Item + 8 + + + Item + 9 + +
    +
    +
    +
    + `); + }); + it('Should render up to 10.5 items (+ 2 buffer) even when there are many more', async () => { + const { baseElement } = await renderInTestApp( + + {[...new Array(100)].map((_, i) => ( + Item {i} + ))} + , + ); + + expect(baseElement.children[0]).toMatchInlineSnapshot(` +
    +
    +
    +
    + + Item + 0 + + + Item + 1 + + + Item + 2 + + + Item + 3 + + + Item + 4 + + + Item + 5 + + + Item + 6 + + + Item + 7 + + + Item + 8 + + + Item + 9 + + + Item + 10 + + + Item + 11 + + + Item + 12 + +
    +
    +
    +
    + `); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx index b0920b626a..6ac6568c2f 100644 --- a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx +++ b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx @@ -17,34 +17,50 @@ import React from 'react'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; +type HTMLDivProps = React.HTMLAttributes; + const renderRow = (props: ListChildComponentProps) => { const { data, index, style } = props; return React.cloneElement(data[index], { style }); }; +// Context needed to keep Autocomplete working correctly : https://v4.mui.com/components/autocomplete/#Virtualize.tsx +const OuterElementContext = React.createContext({}); + +const OuterElementType = React.forwardRef( + (props, ref) => { + const outerProps = React.useContext(OuterElementContext); + return
    ; + }, +); + export const VirtualizedListbox = React.forwardRef< HTMLDivElement, - { children?: React.ReactNode } + HTMLDivProps >((props, ref) => { - const itemData = React.Children.toArray(props.children); + const { children, ...other } = props; + const itemData = React.Children.toArray(children); const itemCount = itemData.length; const itemSize = 36; - const itemsToShow = Math.min(10, itemCount); - const height = Math.max(itemSize, itemsToShow * itemSize - 0.5 * itemSize); + const itemsToShow = Math.min(10, itemCount) + 0.5; + const height = itemsToShow * itemSize; return (
    - - {renderRow} - + + + {renderRow} + +
    ); }); 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..8f65a551e5 100644 --- a/plugins/search-backend-module-catalog/CHANGELOG.md +++ b/plugins/search-backend-module-catalog/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-search-backend-module-catalog +## 0.1.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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..d2597aa27d 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.3", "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..a2c01fd933 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.5.4-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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..f0905099e0 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.3", "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..f2f7930a39 100644 --- a/plugins/search-backend-module-explore/CHANGELOG.md +++ b/plugins/search-backend-module-explore/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-search-backend-module-explore +## 0.1.29-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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 9c6197e8ca..2c6e767668 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.3", "description": "A module for the search backend that exports explore modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 03a33c2081..c57129d4e0 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.33-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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..1595100567 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.3", "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..a480f173af 100644 --- a/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md +++ b/plugins/search-backend-module-stack-overflow-collator/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-search-backend-module-stack-overflow-collator +## 0.1.16-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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 08236889af..a7006fbfac 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.3", "description": "A module for the search backend that exports stack overflow modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-module-techdocs/CHANGELOG.md b/plugins/search-backend-module-techdocs/CHANGELOG.md index 9f5b9e41da..644e080510 100644 --- a/plugins/search-backend-module-techdocs/CHANGELOG.md +++ b/plugins/search-backend-module-techdocs/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-search-backend-module-techdocs +## 0.1.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.3 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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 0dc3a362d3..8e95262f87 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.3", "description": "A module for the search backend that exports techdocs modules", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 9597076338..a87c09cc8e 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-search-backend-node +## 1.2.28-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-tasks@0.5.28-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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/api-report.md b/plugins/search-backend-node/api-report.md index 5e8961a65e..03269c72e8 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -20,7 +20,7 @@ import { SearchQuery } from '@backstage/plugin-search-common'; import { TaskFunction } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; import { Transform } from 'stream'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Writable } from 'stream'; // @public @@ -138,7 +138,7 @@ export class NewlineDelimitedJsonCollatorFactory export type NewlineDelimitedJsonCollatorFactoryOptions = { type: string; searchPattern: string; - reader: UrlReader; + reader: UrlReaderService; logger: LoggerService; visibilityPermission?: Permission; }; diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 01337796b0..a97a0a0a7c 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-node", - "version": "1.2.28-next.1", + "version": "1.2.28-next.3", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", "backstage": { "role": "node-library", @@ -53,7 +53,7 @@ "test": "backstage-cli package test" }, "dependencies": { - "@backstage/backend-common": "workspace:^", + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", "@backstage/config": "workspace:^", diff --git a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts index 45cc98e9ba..ce640636f9 100644 --- a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts +++ b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts @@ -13,17 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { - ReadUrlResponse, - UrlReader, - UrlReaders, -} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { Readable } from 'stream'; import { NewlineDelimitedJsonCollatorFactory } from './NewlineDelimitedJsonCollatorFactory'; import { TestPipeline } from '../test-utils'; import { mockServices } from '@backstage/backend-test-utils'; +import { + UrlReaderService, + UrlReaderServiceReadUrlResponse, +} from '@backstage/backend-plugin-api'; +import { UrlReaders } from '@backstage/backend-defaults/urlReader'; describe('DefaultCatalogCollatorFactory', () => { const config = new ConfigReader({}); @@ -42,7 +41,9 @@ describe('DefaultCatalogCollatorFactory', () => { describe('getCollator', () => { let readable: Readable; let reader: jest.Mocked< - UrlReader & { readUrl: jest.Mock> } + UrlReaderService & { + readUrl: jest.Mock>; + } >; let factory: NewlineDelimitedJsonCollatorFactory; diff --git a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts index e7d749dd41..bf78b98040 100644 --- a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts +++ b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.ts @@ -18,9 +18,8 @@ import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Permission } from '@backstage/plugin-permission-common'; import { Readable } from 'stream'; -import { UrlReader } from '@backstage/backend-common'; import { parse as parseNdjson } from 'ndjson'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; /** * Options for instantiate NewlineDelimitedJsonCollatorFactory @@ -29,7 +28,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; export type NewlineDelimitedJsonCollatorFactoryOptions = { type: string; searchPattern: string; - reader: UrlReader; + reader: UrlReaderService; logger: LoggerService; visibilityPermission?: Permission; }; @@ -73,7 +72,7 @@ export class NewlineDelimitedJsonCollatorFactory private constructor( type: string, private readonly searchPattern: string, - private readonly reader: UrlReader, + private readonly reader: UrlReaderService, private readonly logger: LoggerService, visibilityPermission: Permission | undefined, ) { diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 16e4c8ae4f..c31002bede 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-search-backend +## 1.5.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/backend-defaults@0.4.2-next.3 + - @backstage/backend-openapi-utils@0.1.16-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.3 + - @backstage/plugin-search-backend-node@1.2.28-next.3 + - @backstage/plugin-search-common@1.2.14-next.1 + +## 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 0e39ff2638..4390dd658c 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.3", "description": "The Backstage backend plugin that provides your backstage app with search", "backstage": { "role": "backend-plugin", 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 c58aff0c21..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", diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index f3c5688145..0815bf5b37 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-search-react +## 1.7.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @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-common@1.2.14-next.1 + +## 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..0126fdc590 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -6,6 +6,7 @@ /// import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ListItemProps } from '@material-ui/core/ListItem'; import { PortableSchema } from '@backstage/frontend-plugin-api'; @@ -18,18 +19,26 @@ export type BaseSearchResultListItemProps = T & { result?: SearchDocument; } & Omit; -// @alpha (undocumented) +// @alpha @deprecated export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean; }, >( options: SearchResultItemExtensionOptions, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export namespace createSearchResultListItemExtension { - const // (undocumented) + const // @deprecated (undocumented) itemDataRef: ConfigurableExtensionDataRef< { predicate?: SearchResultItemExtensionPredicate | undefined; @@ -47,7 +56,7 @@ export type SearchResultItemExtensionComponent = < props: P, ) => JSX.Element | null; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export type SearchResultItemExtensionOptions< TConfig extends { noTrack?: boolean; @@ -71,5 +80,48 @@ export type SearchResultItemExtensionPredicate = ( result: SearchResult, ) => boolean; +// @alpha +export const SearchResultListItemBlueprint: ExtensionBlueprint< + 'search-result-list-item', + undefined, + undefined, + SearchResultListItemBlueprintParams, + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + {}, + { + noTrack: boolean; + }, + { + noTrack?: boolean | undefined; + }, + { + item: ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >; + } +>; + +// @alpha (undocumented) +export interface SearchResultListItemBlueprintParams { + component: (options: { + config: { + noTrack?: boolean; + }; + }) => Promise; + predicate?: SearchResultItemExtensionPredicate; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 87ebe36f14..887b41cb83 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.7.14-next.1", + "version": "1.7.14-next.3", "backstage": { "role": "web-library", "pluginId": "search", @@ -28,7 +28,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -36,7 +36,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" diff --git a/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.test.tsx b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.test.tsx new file mode 100644 index 0000000000..4f3b7501b4 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.test.tsx @@ -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 React from 'react'; +import { SearchResultListItemBlueprint } from './SearchResultListItemBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + PageBlueprint, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { searchResultListItemDataRef } from './types'; +import _ from 'lodash'; + +describe('SearchResultListItemBlueprint', () => { + it('should return an extension with sane defaults', () => { + const extension = SearchResultListItemBlueprint.make({ + name: 'test', + params: { + component: async () => () =>
    Hello
    , + predicate: () => true, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:search", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "noTrack": { + "default": false, + "type": "boolean", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "search-result-list-item", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('defaults and passes on config properly', async () => { + const extension = SearchResultListItemBlueprint.make({ + name: 'test', + params: { + component: + async ({ config: { noTrack } }) => + () => +
    noTrack: {String(noTrack)}
    , + }, + }); + + const mockSearchPage = PageBlueprint.makeWithOverrides({ + namespace: 'search', + inputs: { + items: createExtensionInput([searchResultListItemDataRef]), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/', + loader: async () => { + const items = inputs.items.map(i => + i.get(searchResultListItemDataRef), + ); + return ( +
    + {items.map((item, i) => ( + + ))} +
    + ); + }, + }); + }, + }); + + await expect( + createExtensionTester(mockSearchPage) + .add(extension) + .render() + .findByText('noTrack: false'), + ).resolves.toBeInTheDocument(); + + await expect( + createExtensionTester(mockSearchPage) + .add(extension, { config: { noTrack: true } }) + .render() + .findByText('noTrack: true'), + ).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.tsx b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.tsx new file mode 100644 index 0000000000..b9a7b71719 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/SearchResultListItemBlueprint.tsx @@ -0,0 +1,86 @@ +/* + * 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 { + createExtensionBlueprint, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { + SearchResultItemExtensionComponent, + SearchResultItemExtensionPredicate, + searchResultListItemDataRef, +} from './types'; +import { + SearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '../../extensions'; + +/** @alpha */ +export interface SearchResultListItemBlueprintParams { + /** + * The extension component. + */ + component: (options: { + config: { noTrack?: boolean }; + }) => Promise; + /** + * When an extension defines a predicate, it returns true if the result should be rendered by that extension. + * Defaults to a predicate that returns true, which means it renders all sorts of results. + */ + predicate?: SearchResultItemExtensionPredicate; +} + +/** + * @alpha + * Creates SearchResultListItem extensions + */ +export const SearchResultListItemBlueprint = createExtensionBlueprint({ + kind: 'search-result-list-item', + attachTo: { + id: 'page:search', + input: 'items', + }, + config: { + schema: { + noTrack: z => z.boolean().default(false), + }, + }, + output: [searchResultListItemDataRef], + dataRefs: { + item: searchResultListItemDataRef, + }, + *factory(params: SearchResultListItemBlueprintParams, { config, node }) { + const ExtensionComponent = lazy(() => + params.component({ config }).then(component => ({ default: component })), + ); + + yield searchResultListItemDataRef({ + predicate: params.predicate, + component: (props: SearchResultListItemExtensionProps) => ( + + + + + + ), + }); + }, +}); diff --git a/packages/backend-app-api/src/services/implementations/deprecated.ts b/plugins/search-react/src/alpha/blueprints/index.ts similarity index 68% rename from packages/backend-app-api/src/services/implementations/deprecated.ts rename to plugins/search-react/src/alpha/blueprints/index.ts index 347a968bce..b9bbf1012e 100644 --- a/packages/backend-app-api/src/services/implementations/deprecated.ts +++ b/plugins/search-react/src/alpha/blueprints/index.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -export * from './auth'; -export * from './httpAuth'; -export * from './httpRouter'; -export * from './logger'; -export * from './rootHttpRouter'; -export * from './rootLogger'; -export * from './scheduler'; -export * from './userInfo'; +export { + SearchResultListItemBlueprint, + type SearchResultListItemBlueprintParams, +} from './SearchResultListItemBlueprint'; +export { + type BaseSearchResultListItemProps, + type SearchResultItemExtensionComponent, + type SearchResultItemExtensionPredicate, +} from './types'; diff --git a/plugins/search-react/src/alpha/blueprints/types.ts b/plugins/search-react/src/alpha/blueprints/types.ts new file mode 100644 index 0000000000..abfed4aba6 --- /dev/null +++ b/plugins/search-react/src/alpha/blueprints/types.ts @@ -0,0 +1,42 @@ +/* + * 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 { ListItemProps } from '@material-ui/core/ListItem'; +import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; +import { createExtensionDataRef } from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export type BaseSearchResultListItemProps = T & { + rank?: number; + result?: SearchDocument; +} & Omit; + +/** @alpha */ +export type SearchResultItemExtensionComponent = < + P extends BaseSearchResultListItemProps, +>( + props: P, +) => JSX.Element | null; + +/** @alpha */ +export type SearchResultItemExtensionPredicate = ( + result: SearchResult, +) => boolean; + +export const searchResultListItemDataRef = createExtensionDataRef<{ + predicate?: SearchResultItemExtensionPredicate; + component: SearchResultItemExtensionComponent; +}>().with({ id: 'search.search-result-list-item.item' }); diff --git a/plugins/search-react/src/alpha.test.tsx b/plugins/search-react/src/alpha/extensions.test.tsx similarity index 97% rename from plugins/search-react/src/alpha.test.tsx rename to plugins/search-react/src/alpha/extensions.test.tsx index 6d77b5af97..5194fadd1c 100644 --- a/plugins/search-react/src/alpha.test.tsx +++ b/plugins/search-react/src/alpha/extensions.test.tsx @@ -23,10 +23,8 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { SearchResult } from '@backstage/plugin-search-common'; import { screen } from '@testing-library/react'; import React from 'react'; -import { - BaseSearchResultListItemProps, - createSearchResultListItemExtension, -} from './alpha'; +import { createSearchResultListItemExtension } from './extensions'; +import { BaseSearchResultListItemProps } from './blueprints'; describe('createSearchResultListItemExtension', () => { it('Should use the correct result component', async () => { diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha/extensions.tsx similarity index 77% rename from plugins/search-react/src/alpha.tsx rename to plugins/search-react/src/alpha/extensions.tsx index a22eaf4244..db09510a1e 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha/extensions.tsx @@ -15,39 +15,23 @@ */ import React, { lazy } from 'react'; - -import { ListItemProps } from '@material-ui/core/ListItem'; - import { ExtensionBoundary, PortableSchema, createExtension, - createExtensionDataRef, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; -import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; +import { + SearchResultItemExtensionComponent, + SearchResultItemExtensionPredicate, +} from './blueprints'; +import { SearchResultListItemExtension } from '../extensions'; +import { searchResultListItemDataRef } from './blueprints/types'; -import { SearchResultListItemExtension } from './extensions'; - -/** @alpha */ -export type BaseSearchResultListItemProps = T & { - rank?: number; - result?: SearchDocument; -} & Omit; - -/** @alpha */ -export type SearchResultItemExtensionComponent = < - P extends BaseSearchResultListItemProps, ->( - props: P, -) => JSX.Element | null; - -/** @alpha */ -export type SearchResultItemExtensionPredicate = ( - result: SearchResult, -) => boolean; - -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export type SearchResultItemExtensionOptions< TConfig extends { noTrack?: boolean }, > = { @@ -80,7 +64,12 @@ export type SearchResultItemExtensionOptions< predicate?: SearchResultItemExtensionPredicate; }; -/** @alpha */ +/** + * Creates items for the search result list. + * + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean }, >(options: SearchResultItemExtensionOptions) { @@ -132,10 +121,13 @@ export function createSearchResultListItemExtension< }); } -/** @alpha */ +/** + * @alpha + * @deprecated Use {@link SearchResultListItemBlueprint} instead + */ export namespace createSearchResultListItemExtension { - export const itemDataRef = createExtensionDataRef<{ - predicate?: SearchResultItemExtensionPredicate; - component: SearchResultItemExtensionComponent; - }>().with({ id: 'search.search-result-list-item.item' }); + /** + * @deprecated Use {@link SearchResultListItemBlueprint#dataRefs.item} instead + */ + export const itemDataRef = searchResultListItemDataRef; } diff --git a/packages/backend-app-api/src/services/implementations/userInfo/index.ts b/plugins/search-react/src/alpha/index.ts similarity index 89% rename from packages/backend-app-api/src/services/implementations/userInfo/index.ts rename to plugins/search-react/src/alpha/index.ts index 13b42f053c..04026d629d 100644 --- a/packages/backend-app-api/src/services/implementations/userInfo/index.ts +++ b/plugins/search-react/src/alpha/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { userInfoServiceFactory } from './userInfoServiceFactory'; +export * from './extensions'; +export * from './blueprints'; diff --git a/plugins/search-react/src/setupTests.ts b/plugins/search-react/src/setupTests.ts index 992b60d3a4..c47fc7a427 100644 --- a/plugins/search-react/src/setupTests.ts +++ b/plugins/search-react/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +}; diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 5e04d7a198..739cf34487 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-search +## 1.4.15-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @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-common@1.2.14-next.1 + +## 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..8a47cb41fc 100644 --- a/plugins/search/api-report-alpha.md +++ b/plugins/search/api-report-alpha.md @@ -3,42 +3,169 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/core-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; +import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; // @alpha (undocumented) const _default: BackstagePlugin< { root: RouteRef; }, - {} + {}, + { + 'api:search': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + undefined + >; + 'nav-item:search': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined + >; + 'page:search': ExtensionDefinition< + { + noTrack: boolean; + } & { + path: string | undefined; + }, + { + noTrack?: boolean | undefined; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + items: ExtensionInput< + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + { + singleton: false; + optional: false; + } + >; + }, + 'page', + undefined, + undefined + >; + } >; export default _default; // @alpha (undocumented) -export const searchApi: ExtensionDefinition<{}, {}>; +export const searchApi: ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + undefined +>; // @alpha (undocumented) export const searchNavItem: ExtensionDefinition< - { - title: string; - }, - { - title?: string | undefined; - } + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined >; // @alpha (undocumented) export const searchPage: ExtensionDefinition< { - path: string; noTrack: boolean; + } & { + path: string | undefined; }, { - path: string; - noTrack: boolean; - } + noTrack?: boolean | undefined; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + items: ExtensionInput< + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + { + singleton: false; + optional: false; + } + >; + }, + 'page', + undefined, + undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/search/package.json b/plugins/search/package.json index e1cbfcd76a..27f0592de7 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.3", "description": "The Backstage plugin that provides your backstage app with search", "backstage": { "role": "frontend-plugin", diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 838fb95046..90fc394f9d 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -37,12 +37,11 @@ import { } from '@backstage/core-plugin-api'; import { - createPlugin, - createApiExtension, - createPageExtension, + createFrontendPlugin, + ApiBlueprint, createExtensionInput, - createNavItemExtension, - createSchemaFromZod, + PageBlueprint, + NavItemBlueprint, } from '@backstage/frontend-plugin-api'; import { @@ -62,7 +61,7 @@ import { } from '@backstage/plugin-search-react'; import { SearchResult } from '@backstage/plugin-search-common'; import { searchApiRef } from '@backstage/plugin-search-react'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; import { rootRouteRef } from './plugin'; import { SearchClient } from './apis'; @@ -75,13 +74,15 @@ import { } from '@backstage/core-compat-api'; /** @alpha */ -export const searchApi = createApiExtension({ - factory: createApiFactory({ - api: searchApiRef, - deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, - factory: ({ discoveryApi, fetchApi }) => - new SearchClient({ discoveryApi, fetchApi }), - }), +export const searchApi = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: searchApiRef, + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new SearchClient({ discoveryApi, fetchApi }), + }), + }, }); const useSearchPageStyles = makeStyles((theme: Theme) => ({ @@ -97,147 +98,156 @@ const useSearchPageStyles = makeStyles((theme: Theme) => ({ })); /** @alpha */ -export const searchPage = createPageExtension({ - routeRef: convertLegacyRouteRef(rootRouteRef), - configSchema: createSchemaFromZod(z => - z.object({ - path: z.string().default('/search'), - noTrack: z.boolean().default(false), - }), - ), +export const searchPage = PageBlueprint.makeWithOverrides({ + config: { + schema: { + noTrack: z => z.boolean().default(false), + }, + }, inputs: { - items: createExtensionInput({ - item: createSearchResultListItemExtension.itemDataRef, - }), + items: createExtensionInput([SearchResultListItemBlueprint.dataRefs.item]), }, - loader: async ({ config, inputs }) => { - const getResultItemComponent = (result: SearchResult) => { - const value = inputs.items.find(item => - item?.output.item.predicate?.(result), - ); - return value?.output.item.component ?? DefaultResultListItem; - }; + factory(originalFactory, { config, inputs }) { + return originalFactory({ + defaultPath: '/search', + routeRef: convertLegacyRouteRef(rootRouteRef), + loader: async () => { + const getResultItemComponent = (result: SearchResult) => { + const value = inputs.items.find(item => + item + ?.get(SearchResultListItemBlueprint.dataRefs.item) + .predicate?.(result), + ); + return ( + value?.get(SearchResultListItemBlueprint.dataRefs.item).component ?? + DefaultResultListItem + ); + }; - const Component = () => { - const classes = useSearchPageStyles(); - const { isMobile } = useSidebarPinState(); - const { types } = useSearch(); - const catalogApi = useApi(catalogApiRef); + const Component = () => { + const classes = useSearchPageStyles(); + const { isMobile } = useSidebarPinState(); + const { types } = useSearch(); + const catalogApi = useApi(catalogApiRef); - return ( - - {!isMobile &&
    } - - - - - - {!isMobile && ( - - , - }, - { - value: 'techdocs', - name: 'Documentation', - icon: , - }, - ]} - /> - - {types.includes('techdocs') && ( - { - // Return a list of entities which are documented. - const { items } = await catalogApi.getEntities({ - fields: ['metadata.name'], - filter: { - 'metadata.annotations.backstage.io/techdocs-ref': - CATALOG_FILTER_EXISTS, - }, - }); - - const names = items.map( - entity => entity.metadata.name, - ); - names.sort(); - return names; - }} + return ( + + {!isMobile &&
    } + + + + + + {!isMobile && ( + + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ]} /> - )} - - - - - )} - - - - {({ results }) => ( - <> - {results.map((result, index) => { - const { noTrack } = config; - const { document, ...rest } = result; - const SearchResultListItem = - getResultItemComponent(result); - return ( - - ); - })} - - )} - - - - - - - ); - }; + + {types.includes('techdocs') && ( + { + // Return a list of entities which are documented. + const { items } = await catalogApi.getEntities({ + fields: ['metadata.name'], + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + }); - return compatWrapper( - - - - , - ); + const names = items.map( + entity => entity.metadata.name, + ); + names.sort(); + return names; + }} + /> + )} + + + + + )} + + + + {({ results }) => ( + <> + {results.map((result, index) => { + const { noTrack } = config; + const { document, ...rest } = result; + const SearchResultListItem = + getResultItemComponent(result); + return ( + + ); + })} + + )} + + + + + + + ); + }; + + return compatWrapper( + + + + , + ); + }, + }); }, }); /** @alpha */ -export const searchNavItem = createNavItemExtension({ - routeRef: convertLegacyRouteRef(rootRouteRef), - title: 'Search', - icon: SearchIcon, +export const searchNavItem = NavItemBlueprint.make({ + params: { + routeRef: convertLegacyRouteRef(rootRouteRef), + title: 'Search', + icon: SearchIcon, + }, }); /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'search', extensions: [searchApi, searchPage, searchNavItem], routes: convertLegacyRouteRefs({ diff --git a/plugins/signals-backend/CHANGELOG.md b/plugins/signals-backend/CHANGELOG.md index d95a9d74a3..455d1ee0cb 100644 --- a/plugins/signals-backend/CHANGELOG.md +++ b/plugins/signals-backend/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-signals-backend +## 0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + +## 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 05e7189beb..bba7a6a902 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.3", "backstage": { "role": "backend-plugin", "pluginId": "signals", diff --git a/plugins/signals-node/CHANGELOG.md b/plugins/signals-node/CHANGELOG.md index ac1b4d835d..606114c858 100644 --- a/plugins/signals-node/CHANGELOG.md +++ b/plugins/signals-node/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-signals-node +## 0.1.9-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-events-node@0.3.9-next.3 + +## 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..c4a6205944 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.3", "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..67e36f9d7b 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.37-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.3 + - @backstage/plugin-catalog@1.22.0-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @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/test-utils@1.5.10-next.2 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## 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..949ba5e644 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.3", "backstage": { "role": "web-library", "pluginId": "techdocs-addons", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index cc0383d7f4..355ae0703f 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-techdocs-backend +## 1.10.10-next.3 + +### Patch Changes + +- a16632c: Update configuration schema to match actual behavior +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/plugin-techdocs-node@1.12.9-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/catalog-client@1.6.6-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.3 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## 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..4910d8d76e 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -70,16 +70,14 @@ export interface Config { */ publisher?: | { - type: - | 'local' - | 'googleGcs' - | 'awsS3' - | 'azureBlobStorage' - | 'openStackSwift'; + type: 'local'; + /** + * Optional when 'type' is set to local + */ local?: { /** - * Directory to store generated static files. + * (Optional) Directory to store generated static files. */ publishDirectory?: string; }; @@ -132,6 +130,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. diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 448f88e800..074e0be91d 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.3", "description": "The Backstage backend plugin that renders technical documentation for your components", "backstage": { "role": "backend-plugin", diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index 8bfa4e9c49..86010fb23a 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + ## 1.1.13-next.0 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 8254dceaf5..86f60bbf3a 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", - "version": "1.1.13-next.0", + "version": "1.1.13-next.1", "description": "Plugin module for contributed TechDocs Addons", "backstage": { "role": "frontend-plugin-module", diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 40c203014e..a17ba53a5f 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-techdocs-node +## 1.12.9-next.3 + +### Patch Changes + +- 5cedd9f: Fix TechDocs Edit URL for nested docs +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## 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/api-report.md b/plugins/techdocs-node/api-report.md index f03f9e1b0e..6c7f05e900 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -15,7 +15,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import * as winston from 'winston'; import { Writable } from 'stream'; @@ -82,7 +82,7 @@ export class Generators implements GeneratorBuilder { // @public export const getDocFilesFromRepository: ( - reader: UrlReader, + reader: UrlReaderService, entity: Entity, opts?: { etag?: string; @@ -157,7 +157,7 @@ export type PreparerBuilder = { // @public export type PreparerConfig = { logger: Logger; - reader: UrlReader; + reader: UrlReaderService; }; // @public diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 8553169546..709803af84 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.3", "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/helpers.test.ts b/plugins/techdocs-node/src/helpers.test.ts index 4f77a5432b..33f4018f47 100644 --- a/plugins/techdocs-node/src/helpers.test.ts +++ b/plugins/techdocs-node/src/helpers.test.ts @@ -14,13 +14,6 @@ * limitations under the License. */ -import { - ReadTreeResponse, - ReadUrlOptions, - ReadUrlResponse, - SearchResponse, - UrlReader, -} from '@backstage/backend-common'; import { Entity, getEntitySourceLocation } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -34,6 +27,13 @@ import { parseReferenceAnnotation, transformDirLocation, } from './helpers'; +import { + UrlReaderService, + UrlReaderServiceSearchResponse, + UrlReaderServiceReadUrlResponse, + UrlReaderServiceReadUrlOptions, + UrlReaderServiceReadTreeResponse, +} from '@backstage/backend-plugin-api'; jest.mock('@backstage/catalog-model', () => ({ ...jest.requireActual('@backstage/catalog-model'), @@ -290,19 +290,19 @@ describe('getLocationForEntity', () => { describe('getDocFilesFromRepository', () => { it('should read a remote directory using UrlReader.readTree', async () => { - class MockUrlReader implements UrlReader { + class MockUrlReader implements UrlReaderService { async read() { return Buffer.from('mock'); } async readUrl( _url: string, - _options?: ReadUrlOptions | undefined, - ): Promise { + _options?: UrlReaderServiceReadUrlOptions | undefined, + ): Promise { throw new Error('Method not implemented.'); } - async readTree(): Promise { + async readTree(): Promise { return { dir: async () => { return '/tmp/testfolder'; @@ -317,7 +317,7 @@ describe('getDocFilesFromRepository', () => { }; } - async search(): Promise { + async search(): Promise { return { etag: '', files: [], diff --git a/plugins/techdocs-node/src/helpers.ts b/plugins/techdocs-node/src/helpers.ts index d64c7fc289..38913e8e9b 100644 --- a/plugins/techdocs-node/src/helpers.ts +++ b/plugins/techdocs-node/src/helpers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { Entity, @@ -143,7 +143,7 @@ export const getLocationForEntity = ( * @param opts - Options for configuring the reader, e.g. logger, etag, etc. */ export const getDocFilesFromRepository = async ( - reader: UrlReader, + reader: UrlReaderService, entity: Entity, opts?: { etag?: string; logger?: Logger }, ): Promise => { 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-node/src/stages/prepare/dir.test.ts b/plugins/techdocs-node/src/stages/prepare/dir.test.ts index a26c423aaf..df470d7da6 100644 --- a/plugins/techdocs-node/src/stages/prepare/dir.test.ts +++ b/plugins/techdocs-node/src/stages/prepare/dir.test.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { UrlReader, loggerToWinstonLogger } from '@backstage/backend-common'; +import { loggerToWinstonLogger } from '@backstage/backend-common'; import { TECHDOCS_ANNOTATION } from '@backstage/plugin-techdocs-common'; import { ConfigReader } from '@backstage/config'; import { DirectoryPreparer } from './dir'; import { mockServices } from '@backstage/backend-test-utils'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; function normalizePath(path: string) { return path @@ -46,7 +47,7 @@ const createMockEntity = (annotations: {}) => { }; const mockConfig = new ConfigReader({}); -const mockUrlReader: jest.Mocked = { +const mockUrlReader: jest.Mocked = { readUrl: jest.fn(), readTree: jest.fn(), search: jest.fn(), diff --git a/plugins/techdocs-node/src/stages/prepare/dir.ts b/plugins/techdocs-node/src/stages/prepare/dir.ts index 1c18b17a1e..cecd2564e6 100644 --- a/plugins/techdocs-node/src/stages/prepare/dir.ts +++ b/plugins/techdocs-node/src/stages/prepare/dir.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; @@ -38,7 +38,7 @@ import { */ export class DirectoryPreparer implements PreparerBase { private readonly scmIntegrations: ScmIntegrationRegistry; - private readonly reader: UrlReader; + private readonly reader: UrlReaderService; /** * Returns a directory preparer instance @@ -55,7 +55,7 @@ export class DirectoryPreparer implements PreparerBase { private constructor( config: Config, _logger: Logger | null, - reader: UrlReader, + reader: UrlReaderService, ) { this.reader = reader; this.scmIntegrations = ScmIntegrations.fromConfig(config); diff --git a/plugins/techdocs-node/src/stages/prepare/types.ts b/plugins/techdocs-node/src/stages/prepare/types.ts index 79cbf68a00..14ec185c97 100644 --- a/plugins/techdocs-node/src/stages/prepare/types.ts +++ b/plugins/techdocs-node/src/stages/prepare/types.ts @@ -15,7 +15,7 @@ */ import type { Entity } from '@backstage/catalog-model'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Logger } from 'winston'; /** @@ -30,7 +30,7 @@ export type ETag = string; */ export type PreparerConfig = { logger: Logger; - reader: UrlReader; + reader: UrlReaderService; }; /** diff --git a/plugins/techdocs-node/src/stages/prepare/url.ts b/plugins/techdocs-node/src/stages/prepare/url.ts index 84ea1ccda0..24480d2703 100644 --- a/plugins/techdocs-node/src/stages/prepare/url.ts +++ b/plugins/techdocs-node/src/stages/prepare/url.ts @@ -15,7 +15,7 @@ */ import { assertError } from '@backstage/errors'; -import { UrlReader } from '@backstage/backend-common'; +import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { Logger } from 'winston'; import { getDocFilesFromRepository } from '../../helpers'; @@ -32,7 +32,7 @@ import { */ export class UrlPreparer implements PreparerBase { private readonly logger: Logger; - private readonly reader: UrlReader; + private readonly reader: UrlReaderService; /** * Returns a directory preparer instance @@ -42,7 +42,7 @@ export class UrlPreparer implements PreparerBase { return new UrlPreparer(options.reader, options.logger); } - private constructor(reader: UrlReader, logger: Logger) { + private constructor(reader: UrlReaderService, logger: Logger) { this.logger = logger; this.reader = reader; } diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index 20301266cc..eadb950e84 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-react +## 1.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + ## 1.2.7-next.0 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index ae1f72c8c9..dc21b720f4 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-react", - "version": "1.2.7-next.0", + "version": "1.2.7-next.1", "description": "Shared frontend utilities for TechDocs and Addons", "backstage": { "role": "web-library", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 0318bf45aa..3f9471f12c 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-techdocs +## 1.10.8-next.3 + +### Patch Changes + +- 27794d1: 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. +- 8543e72: TechDocs redirect feature now includes a notification to the user before they are redirected. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/catalog-model@1.6.0-next.0 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @backstage/plugin-search-react@1.7.14-next.3 + - @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@1.14.0-next.0 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.1 + +## 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..14a2400534 100644 --- a/plugins/techdocs/api-report-alpha.md +++ b/plugins/techdocs/api-report-alpha.md @@ -3,9 +3,19 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; +import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; // @alpha (undocumented) const _default: BackstagePlugin< @@ -18,26 +28,183 @@ const _default: BackstagePlugin< }>; entityContent: RouteRef; }, - {} + {}, + { + 'nav-item:techdocs': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined + >; + 'api:techdocs/storage': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef, + {}, + 'api', + undefined, + 'storage' + >; + 'search-result-list-item:techdocs': ExtensionDefinition< + { + title: string | undefined; + lineClamp: number; + asLink: boolean; + asListItem: boolean; + } & { + noTrack: boolean; + }, + { + title?: string | undefined; + lineClamp?: number | undefined; + asListItem?: boolean | undefined; + asLink?: boolean | undefined; + } & { + noTrack?: boolean | undefined; + }, + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'search-result-list-item', + undefined, + undefined + >; + 'page:techdocs/reader': ExtensionDefinition< + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + {}, + 'page', + undefined, + 'reader' + >; + 'entity-content:techdocs': ExtensionDefinition< + { + path: string | undefined; + title: string | undefined; + filter: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, + {}, + 'entity-content', + undefined, + undefined + >; + } >; export default _default; // @alpha (undocumented) export const techDocsSearchResultListItemExtension: ExtensionDefinition< { + title: string | undefined; lineClamp: number; - noTrack: boolean; - asListItem: boolean; asLink: boolean; - title?: string | undefined; + asListItem: boolean; + } & { + noTrack: boolean; }, { - lineClamp: number; - noTrack: boolean; - asListItem: boolean; - asLink: boolean; title?: string | undefined; - } + lineClamp?: number | undefined; + asListItem?: boolean | undefined; + asLink?: boolean | undefined; + } & { + noTrack?: boolean | undefined; + }, + ConfigurableExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate | undefined; + component: SearchResultItemExtensionComponent; + }, + 'search.search-result-list-item.item', + {} + >, + { + [x: string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + 'search-result-list-item', + undefined, + 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..9a9586008d 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.3", "description": "The Backstage plugin that renders technical documentation for your components", "backstage": { "role": "frontend-plugin", diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index 07fd7667ff..b0d0d3d467 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -17,13 +17,12 @@ import React from 'react'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import { - createPlugin, - createSchemaFromZod, - createApiExtension, - createPageExtension, - createNavItemExtension, + createFrontendPlugin, + ApiBlueprint, + PageBlueprint, + NavItemBlueprint, } from '@backstage/frontend-plugin-api'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; import { configApiRef, createApiFactory, @@ -45,64 +44,73 @@ import { rootDocsRouteRef, rootRouteRef, } from './routes'; -import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; /** @alpha */ -const techDocsStorageApi = createApiExtension({ - factory: createApiFactory({ - api: techdocsStorageApiRef, - deps: { - configApi: configApiRef, - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ configApi, discoveryApi, fetchApi }) => - new TechDocsStorageClient({ - configApi, - discoveryApi, - fetchApi, - }), - }), +const techDocsStorageApi = ApiBlueprint.make({ + name: 'storage', + params: { + factory: createApiFactory({ + api: techdocsStorageApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ configApi, discoveryApi, fetchApi }) => + new TechDocsStorageClient({ + configApi, + discoveryApi, + fetchApi, + }), + }), + }, }); /** @alpha */ -const techDocsClientApi = createApiExtension({ - factory: createApiFactory({ - api: techdocsApiRef, - deps: { - configApi: configApiRef, - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ configApi, discoveryApi, fetchApi }) => - new TechDocsClient({ - configApi, - discoveryApi, - fetchApi, - }), - }), +const techDocsClientApi = ApiBlueprint.make({ + params: { + factory: createApiFactory({ + api: techdocsApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ configApi, discoveryApi, fetchApi }) => + new TechDocsClient({ + configApi, + discoveryApi, + fetchApi, + }), + }), + }, }); /** @alpha */ export const techDocsSearchResultListItemExtension = - createSearchResultListItemExtension({ - configSchema: createSchemaFromZod(z => - z.object({ - // TODO: Define how the icon can be configurable - title: z.string().optional(), - lineClamp: z.number().default(5), - asLink: z.boolean().default(true), - asListItem: z.boolean().default(true), - noTrack: z.boolean().default(false), - }), - ), - predicate: result => result.type === 'techdocs', - component: async ({ config }) => { - const { TechDocsSearchResultListItem } = await import( - './search/components/TechDocsSearchResultListItem' - ); - return props => - compatWrapper(); + SearchResultListItemBlueprint.makeWithOverrides({ + config: { + schema: { + title: z => z.string().optional(), + lineClamp: z => z.number().default(5), + asLink: z => z.boolean().default(true), + asListItem: z => z.boolean().default(true), + }, + }, + factory(originalFactory, { config }) { + return originalFactory({ + predicate: result => result.type === 'techdocs', + component: async () => { + const { TechDocsSearchResultListItem } = await import( + './search/components/TechDocsSearchResultListItem' + ); + return props => + compatWrapper( + , + ); + }, + }); }, }); @@ -111,13 +119,15 @@ export const techDocsSearchResultListItemExtension = * * @alpha */ -const techDocsPage = createPageExtension({ - defaultPath: '/docs', - routeRef: convertLegacyRouteRef(rootRouteRef), - loader: () => - import('./home/components/TechDocsIndexPage').then(m => - compatWrapper(), - ), +const techDocsPage = PageBlueprint.make({ + params: { + defaultPath: '/docs', + routeRef: convertLegacyRouteRef(rootRouteRef), + loader: () => + import('./home/components/TechDocsIndexPage').then(m => + compatWrapper(), + ), + }, }); /** @@ -125,14 +135,16 @@ const techDocsPage = createPageExtension({ * * @alpha */ -const techDocsReaderPage = createPageExtension({ +const techDocsReaderPage = PageBlueprint.make({ name: 'reader', - defaultPath: '/docs/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(rootDocsRouteRef), - loader: () => - import('./reader/components/TechDocsReaderPage').then(m => - compatWrapper(), - ), + params: { + defaultPath: '/docs/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(rootDocsRouteRef), + loader: () => + import('./reader/components/TechDocsReaderPage').then(m => + compatWrapper(), + ), + }, }); /** @@ -140,22 +152,26 @@ const techDocsReaderPage = createPageExtension({ * * @alpha */ -const techDocsEntityContent = createEntityContentExtension({ - defaultPath: 'docs', - defaultTitle: 'TechDocs', - loader: () => - import('./Router').then(m => compatWrapper()), +const techDocsEntityContent = EntityContentBlueprint.make({ + params: { + defaultPath: 'docs', + defaultTitle: 'TechDocs', + loader: () => + import('./Router').then(m => compatWrapper()), + }, }); /** @alpha */ -const techDocsNavItem = createNavItemExtension({ - icon: LibraryBooks, - title: 'Docs', - routeRef: convertLegacyRouteRef(rootRouteRef), +const techDocsNavItem = NavItemBlueprint.make({ + params: { + icon: LibraryBooks, + title: 'Docs', + routeRef: convertLegacyRouteRef(rootRouteRef), + }, }); /** @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'techdocs', extensions: [ techDocsClientApi, 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..28090cb62c 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -44,6 +44,7 @@ import { copyToClipboard, useSanitizerTransformer, useStylesTransformer, + handleMetaRedirects, } from '../../transformers'; import { useNavigateUrl } from './useNavigateUrl'; @@ -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/components/TechDocsRedirectNotification/TechDocsRedirectNotification.tsx b/plugins/techdocs/src/reader/components/TechDocsRedirectNotification/TechDocsRedirectNotification.tsx new file mode 100644 index 0000000000..b233549b9e --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsRedirectNotification/TechDocsRedirectNotification.tsx @@ -0,0 +1,67 @@ +/* + * 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 { makeStyles } from '@material-ui/core/styles'; +import React, { useState } from 'react'; +import Snackbar from '@material-ui/core/Snackbar'; +import Button from '@material-ui/core/Button'; + +type TechDocsRedirectNotificationProps = { + handleButtonClick: () => void; + message: string; + autoHideDuration: number; +}; + +const useStyles = makeStyles(theme => ({ + button: { + color: theme.palette.primary.light, + textDecoration: 'underline', + }, +})); + +export const TechDocsRedirectNotification = ({ + message, + handleButtonClick, + autoHideDuration, +}: TechDocsRedirectNotificationProps) => { + const classes = useStyles(); + const [open, setOpen] = useState(true); + + const handleClose = () => setOpen(false); + + return ( + { + handleClose(); + handleButtonClick(); + }} + > + Redirect now + + } + /> + ); +}; diff --git a/packages/backend-app-api/src/services/implementations/database/index.ts b/plugins/techdocs/src/reader/components/TechDocsRedirectNotification/index.ts similarity index 82% rename from packages/backend-app-api/src/services/implementations/database/index.ts rename to plugins/techdocs/src/reader/components/TechDocsRedirectNotification/index.ts index d676c8013e..fe94a25bd6 100644 --- a/packages/backend-app-api/src/services/implementations/database/index.ts +++ b/plugins/techdocs/src/reader/components/TechDocsRedirectNotification/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { databaseServiceFactory } from './databaseServiceFactory'; +export { TechDocsRedirectNotification } from './TechDocsRedirectNotification'; 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..0be1773846 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts @@ -0,0 +1,116 @@ +/* + * 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( + 'http://localhost/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..04b7b27ba2 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx @@ -0,0 +1,88 @@ +/* + * 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 React from 'react'; +import { renderReactElement } from './renderReactElement'; +import { TechDocsRedirectNotification } from '../components/TechDocsRedirectNotification'; + +export const handleMetaRedirects = ( + navigate: (to: string) => void, + entityName: string, +): Transformer => { + const redirectAfterMs = 3000; + + const determineRedirectURL = (metaUrl: string) => { + const normalizedCurrentUrl = normalizeUrl(window.location.href); + // When creating URL object, if the metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href. + 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 new URL(siteHomePath, normalizedCurrentUrl).href; + } + 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) { + return dom; + } + + const metaUrl = metaContentParameters[1]; + const redirectURL = determineRedirectURL(metaUrl); + + // If the current URL is the same as the redirect URL, do not proceed with the redirect. + if (window.location.href === redirectURL) { + return dom; + } + + 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/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index 7df98f1f26..7fdf47b009 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -27,3 +27,4 @@ export * from './simplifyMkdocsFooter'; export * from './onCssReady'; export * from './scrollIntoNavigation'; export * from './transformer'; +export * from './handleMetaRedirects'; diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 9101a22bef..af615afdfb 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-user-settings-backend +## 0.2.22-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.3 + - @backstage/backend-common@0.23.4-next.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.5.0-next.3 + - @backstage/plugin-signals-node@0.1.9-next.3 + - @backstage/plugin-user-settings-common@0.0.1 + +## 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..f93bb24a33 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.3", "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..4f007e3da0 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-user-settings +## 0.8.11-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.3 + - @backstage/core-compat-api@0.2.8-next.3 + - @backstage/plugin-catalog-react@1.12.3-next.3 + - @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.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..b79657cabc 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -3,8 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; @@ -13,18 +18,87 @@ const _default: BackstagePlugin< { root: RouteRef; }, - {} + {}, + { + 'nav-item:user-settings': ExtensionDefinition< + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined + >; + 'page:user-settings': ExtensionDefinition< + { + [x: string]: any; + } & { + path: string | undefined; + }, + { + [x: string]: any; + } & { + path?: string | undefined; + }, + | ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + > + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >, + { + providerSettings: ExtensionInput< + ConfigurableExtensionDataRef< + React_2.JSX.Element, + 'core.reactElement', + {} + >, + { + singleton: true; + optional: true; + } + >; + }, + 'page', + undefined, + undefined + >; + } >; export default _default; // @alpha (undocumented) export const settingsNavItem: ExtensionDefinition< - { - title: string; - }, - { - title?: string | undefined; - } + {}, + {}, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + 'nav-item', + undefined, + undefined >; // @alpha (undocumented) diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index b229e625b6..756555f8ef 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.3", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx index f14db7233d..bdd16acf0f 100644 --- a/plugins/user-settings/src/alpha.tsx +++ b/plugins/user-settings/src/alpha.tsx @@ -16,9 +16,9 @@ import { coreExtensionData, createExtensionInput, - createNavItemExtension, - createPageExtension, - createPlugin, + createFrontendPlugin, + PageBlueprint, + NavItemBlueprint, } from '@backstage/frontend-plugin-api'; import { convertLegacyRouteRef, @@ -32,38 +32,44 @@ import React from 'react'; export * from './translation'; -const userSettingsPage = createPageExtension({ - defaultPath: '/settings', - routeRef: convertLegacyRouteRef(settingsRouteRef), +const userSettingsPage = PageBlueprint.makeWithOverrides({ inputs: { - providerSettings: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true, optional: true }, - ), + providerSettings: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + optional: true, + }), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + defaultPath: '/settings', + routeRef: convertLegacyRouteRef(settingsRouteRef), + loader: () => + import('./components/SettingsPage').then(m => + compatWrapper( + , + ), + ), + }); }, - loader: ({ inputs }) => - import('./components/SettingsPage').then(m => - compatWrapper( - , - ), - ), }); /** @alpha */ -export const settingsNavItem = createNavItemExtension({ - routeRef: convertLegacyRouteRef(settingsRouteRef), - title: 'Settings', - icon: SettingsIcon, +export const settingsNavItem = NavItemBlueprint.make({ + params: { + routeRef: convertLegacyRouteRef(settingsRouteRef), + title: 'Settings', + icon: SettingsIcon, + }, }); /** * @alpha */ -export default createPlugin({ +export default createFrontendPlugin({ id: 'user-settings', extensions: [userSettingsPage, settingsNavItem], routes: convertLegacyRouteRefs({ diff --git a/storybook/package.json b/storybook/package.json index 46cf09dc79..60ccd0d4ea 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -4,14 +4,18 @@ "description": "Storybook build for components package", "private": true, "scripts": { - "start": "start-storybook -p 6006", - "build-storybook": "build-storybook --output-dir dist" + "build-storybook": "build-storybook --output-dir dist", + "start": "start-storybook -p 6006" + }, + "resolutions": { + "webpack": "^5.73.0" }, "dependencies": { "@swc/core": "^1.3.46", "react": "^18.0.2", "react-dom": "^18.0.2", "react-hot-loader": "^4.13.0", + "react-router-dom": "^6.25.1", "swc-loader": "^0.2.3" }, "devDependencies": { @@ -29,9 +33,6 @@ "storybook-dark-mode": "^1.1.0", "typescript": "~4.9.0" }, - "resolutions": { - "webpack": "^5.73.0" - }, "peerDependencies": { "@backstage/core-app-api": "*", "@backstage/core-plugin-api": "*", diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 28bbe4b144..9cd8e2075e 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -1802,6 +1802,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.18.0": + version: 1.18.0 + resolution: "@remix-run/router@npm:1.18.0" + checksum: dad8e8cbd3b44503c89de0fc5d8f92954d04c00bf36b42a18ae8b03fe0783742a99d0cae36f2962db38e2bdba3d40259105202c69620943e88f81711ceeb8fe4 + languageName: node + linkType: hard + "@storybook/addon-a11y@npm:^6.5.9": version: 6.5.16 resolution: "@storybook/addon-a11y@npm:6.5.16" @@ -9684,6 +9691,30 @@ __metadata: languageName: node linkType: hard +"react-router-dom@npm:^6.25.1": + version: 6.25.1 + resolution: "react-router-dom@npm:6.25.1" + dependencies: + "@remix-run/router": 1.18.0 + react-router: 6.25.1 + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: fac768bc02cc0f39f5c1f022c8e7323ce9ffcc9b8e80456b51a70ff765854444cb4e6cdbf7d4132064a8fcbeeabeb82b8802614fbe66d8cbe7408e23ee33d789 + languageName: node + linkType: hard + +"react-router@npm:6.25.1": + version: 6.25.1 + resolution: "react-router@npm:6.25.1" + dependencies: + "@remix-run/router": 1.18.0 + peerDependencies: + react: ">=16.8" + checksum: 2abbab5ecc5d1210f70980571326fd1956da3898b78bfcce6229e346e658dd42ec75bea1caea6aa45607ef677e442f3241dc3fb839293f276fa4bd466870aadd + languageName: node + linkType: hard + "react-sizeme@npm:^3.0.1": version: 3.0.2 resolution: "react-sizeme@npm:3.0.2" @@ -10684,6 +10715,7 @@ __metadata: react: ^18.0.2 react-dom: ^18.0.2 react-hot-loader: ^4.13.0 + react-router-dom: ^6.25.1 storybook-dark-mode: ^1.1.0 swc-loader: ^0.2.3 typescript: ~4.9.0 diff --git a/yarn.lock b/yarn.lock index c1dc608e87..301d7de22f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3478,8 +3478,6 @@ __metadata: "@backstage/types": "workspace:^" "@manypkg/get-packages": ^1.1.3 "@types/compression": ^1.7.0 - "@types/cors": ^2.8.6 - "@types/express": ^4.17.6 "@types/fs-extra": ^11.0.0 "@types/http-errors": ^2.0.0 "@types/minimist": ^1.2.0 @@ -3649,6 +3647,7 @@ __metadata: "@types/http-errors": ^2.0.0 "@types/morgan": ^1.9.0 "@types/node-forge": ^1.3.0 + "@types/pg-format": ^1.0.5 "@types/stoppable": ^1.1.0 archiver: ^6.0.0 aws-sdk-client-mock: ^4.0.0 @@ -3683,6 +3682,7 @@ __metadata: path-to-regexp: ^6.2.1 pg: ^8.11.3 pg-connection-string: ^2.3.0 + pg-format: ^1.0.4 raw-body: ^2.4.1 selfsigned: ^2.0.0 stoppable: ^1.1.0 @@ -3713,6 +3713,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -3844,7 +3845,19 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-client@^1.6.5, @backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": +"@backstage/catalog-client@npm:^1.6.5": + version: 1.6.5 + resolution: "@backstage/catalog-client@npm:1.6.5" + dependencies: + "@backstage/catalog-model": ^1.5.0 + "@backstage/errors": ^1.2.4 + cross-fetch: ^4.0.0 + uri-template: ^2.0.0 + checksum: afb84382c7a8e9124090d56ec4a3a1e8ab7dfda33d337851412b9ee4fca0e85fb7263729d6eb4efa8c3198343ed03843ef468492f74401951542908534febfad + languageName: node + linkType: hard + +"@backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": version: 0.0.0-use.local resolution: "@backstage/catalog-client@workspace:packages/catalog-client" dependencies: @@ -3857,7 +3870,19 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.5.0, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": +"@backstage/catalog-model@npm:^1.4.3, @backstage/catalog-model@npm:^1.5.0": + version: 1.5.0 + resolution: "@backstage/catalog-model@npm:1.5.0" + dependencies: + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + ajv: ^8.10.0 + lodash: ^4.17.21 + checksum: 545873625afbb25a2142af9f8c701547b448fe8b822c9ed699c86a9c385571014115a2c3105a3dca2bc2ac63b837b093dba39a973c2f9e23521d427a0328ba12 + languageName: node + linkType: hard + +"@backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": version: 0.0.0-use.local resolution: "@backstage/catalog-model@workspace:packages/catalog-model" dependencies: @@ -3922,7 +3947,7 @@ __metadata: "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": ^1.1.3 - "@module-federation/enhanced": ^0.1.19 + "@module-federation/enhanced": ^0.3.5 "@octokit/graphql": ^5.0.0 "@octokit/graphql-schema": ^13.7.0 "@octokit/oauth-app": ^4.2.0 @@ -4171,6 +4196,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 @@ -4696,7 +4722,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 @@ -5264,6 +5290,7 @@ __metadata: "@aws-sdk/types": ^3.347.0 "@aws-sdk/util-stream-node": ^3.350.0 "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -5894,6 +5921,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" @@ -5974,6 +6002,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" @@ -6304,6 +6333,7 @@ __metadata: "@azure/identity": ^4.0.0 "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" @@ -6403,6 +6433,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" @@ -6953,6 +6984,7 @@ __metadata: resolution: "@backstage/plugin-scaffolder-backend-module-cookiecutter@workspace:plugins/scaffolder-backend-module-cookiecutter" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -7151,6 +7183,7 @@ __metadata: dependencies: "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -7178,6 +7211,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" + "@opentelemetry/api": ^1.3.0 "@types/express": ^4.17.6 "@types/fs-extra": ^11.0.0 "@types/luxon": ^3.0.0 @@ -7526,6 +7560,7 @@ __metadata: resolution: "@backstage/plugin-search-backend-node@workspace:plugins/search-backend-node" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -9612,7 +9647,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: @@ -9639,9 +9674,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 @@ -9651,7 +9713,7 @@ __metadata: peerDependenciesMeta: graphql-ws: optional: true - checksum: 5328426051b7f9a9ffbd569c950d1a103ce0e2ee7b5d7a57f3d899488ad43d1a5101e8aeced7416e106c7687d67bb7981aa7e87dea5b0f17b77569aa738bf3b5 + checksum: 4474a998a948d0df5cef8371245b0e895be40803a07e49231ab91798aa761880e6e4a684478a44b723c013f0e8206dab3d86e5b0fc9b688df035fa9f1fa29734 languageName: node linkType: hard @@ -11104,13 +11166,22 @@ __metadata: languageName: node linkType: hard -"@module-federation/dts-plugin@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/dts-plugin@npm:0.1.21" +"@module-federation/bridge-react-webpack-plugin@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.3.5" dependencies: - "@module-federation/managers": 0.1.21 - "@module-federation/sdk": 0.1.21 - "@module-federation/third-party-dts-extractor": 0.1.21 + "@module-federation/sdk": 0.3.5 + checksum: 9fb0aab200bd63a07c1adcc71e3342c713de4806dccc2b4a8ea2eb19677b2c8753b638906ed48ab49909c753319f33792cb2afd08c0d6a80b4af742d1dc57bd1 + languageName: node + linkType: hard + +"@module-federation/dts-plugin@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/dts-plugin@npm:0.3.5" + dependencies: + "@module-federation/managers": 0.3.5 + "@module-federation/sdk": 0.3.5 + "@module-federation/third-party-dts-extractor": 0.3.5 adm-zip: ^0.5.10 ansi-colors: ^4.1.3 axios: ^1.6.7 @@ -11122,31 +11193,33 @@ __metadata: log4js: 6.9.1 node-schedule: 2.1.1 rambda: ^9.1.0 - ws: 8.17.0 + ws: 8.17.1 peerDependencies: typescript: ^4.9.0 || ^5.0.0 - vue-tsc: ^1.0.24 + vue-tsc: ">=1.0.24" peerDependenciesMeta: vue-tsc: optional: true - checksum: ec4cd030a25617698754cbac2da5463f8942cdd0a64bdc95f6ff5fd29fff6b88cf3db90e53e6b260cd3593893fe6ee2d6e149d0a46698eb7f9cc19a9df26193d + checksum: 8c8e653d0818b3df14ec399d6db0b5784f202c7909112e4884f29c4e268bd9ff6b2d891677776a65ac6204bf4df9cb2f377b7cb8884c1fee98d275d8d2f28f29 languageName: node linkType: hard -"@module-federation/enhanced@npm:^0.1.19": - version: 0.1.21 - resolution: "@module-federation/enhanced@npm:0.1.21" +"@module-federation/enhanced@npm:^0.3.5": + version: 0.3.5 + resolution: "@module-federation/enhanced@npm:0.3.5" dependencies: - "@module-federation/dts-plugin": 0.1.21 - "@module-federation/managers": 0.1.21 - "@module-federation/manifest": 0.1.21 - "@module-federation/rspack": 0.1.21 - "@module-federation/runtime-tools": 0.1.21 - "@module-federation/sdk": 0.1.21 + "@module-federation/bridge-react-webpack-plugin": 0.3.5 + "@module-federation/dts-plugin": 0.3.5 + "@module-federation/managers": 0.3.5 + "@module-federation/manifest": 0.3.5 + "@module-federation/rspack": 0.3.5 + "@module-federation/runtime-tools": 0.3.5 + "@module-federation/sdk": 0.3.5 + btoa: ^1.2.1 upath: 2.0.1 peerDependencies: typescript: ^4.9.0 || ^5.0.0 - vue-tsc: ^1.0.24 + vue-tsc: ">=1.0.24" webpack: ^5.0.0 peerDependenciesMeta: typescript: @@ -11155,91 +11228,100 @@ __metadata: optional: true webpack: optional: true - checksum: bc0ff541db1066b290b3ad9ab868437dc3d0754b9d06ab263f8fb7f54e08238eae9232a482a681dd6152e4a175578d65df9f27fe181fc8fb602d7cc1ae34807d + checksum: 40130aa86c1ae2c90cf49360754f4e9688f33900087d376215c14b64d1f596141785eb7794457bb76387bb79725a60bee1271e3911d95183e9379c6454145894 languageName: node linkType: hard -"@module-federation/managers@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/managers@npm:0.1.21" +"@module-federation/managers@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/managers@npm:0.3.5" dependencies: - "@module-federation/sdk": 0.1.21 + "@module-federation/sdk": 0.3.5 find-pkg: 2.0.0 fs-extra: 9.1.0 - checksum: 5f230d5795d86dfd68c404ee2b7a1264950c283a4b1c6f4ee9cc9579fabb413718dfbc1ff726b9c213f9d3223d944dd38dd9d04b700962e6398c3c3728d6323e + checksum: a60a21c2c9bf84fd0911365980e2f0e163485f0a952992e3fec242e8e7a33f0c803c62e1c463fb1326845f0077aef2d45d90eba0f6f69545e60e0ea708ebcaba languageName: node linkType: hard -"@module-federation/manifest@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/manifest@npm:0.1.21" +"@module-federation/manifest@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/manifest@npm:0.3.5" dependencies: - "@module-federation/dts-plugin": 0.1.21 - "@module-federation/managers": 0.1.21 - "@module-federation/sdk": 0.1.21 + "@module-federation/dts-plugin": 0.3.5 + "@module-federation/managers": 0.3.5 + "@module-federation/sdk": 0.3.5 chalk: 3.0.0 find-pkg: 2.0.0 - checksum: cef2011875f14e853a355626ae1dbc8ae3b0714d31140e329b5dd71525782b08c2e1d6ca45276a563bb3c3b7f7c4e64a31f0698ef12606f05aa6da46e759f345 + checksum: 0b0f41802630536a9a90b3e958052d2a1503bdd3927c40e1962e371ee26f2646172ef895b868fd287921a318e0bac5a513ef069517565c445fb659c150e3a257 languageName: node linkType: hard -"@module-federation/rspack@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/rspack@npm:0.1.21" +"@module-federation/rspack@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/rspack@npm:0.3.5" dependencies: - "@module-federation/dts-plugin": 0.1.21 - "@module-federation/managers": 0.1.21 - "@module-federation/manifest": 0.1.21 - "@module-federation/runtime-tools": 0.1.21 - "@module-federation/sdk": 0.1.21 - checksum: 55516285e23f4ca7127afafb14af667defbe46dc3224f85d7e07edbc8937d7fac909dfebc2f9dd73120b99bbe5135372cf0fbbe282990d80e6953a60dfa4c93e + "@module-federation/bridge-react-webpack-plugin": 0.3.5 + "@module-federation/dts-plugin": 0.3.5 + "@module-federation/managers": 0.3.5 + "@module-federation/manifest": 0.3.5 + "@module-federation/runtime-tools": 0.3.5 + "@module-federation/sdk": 0.3.5 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ">=1.0.24" + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + checksum: 0afd72784ebbc92ab5d295de1be938cec259f7fcdc59b753af881bbf5b7f982b816c9c04b1ee5e052326556b8a4647001a623c58562c908ee7ffe316f4171df6 languageName: node linkType: hard -"@module-federation/runtime-tools@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/runtime-tools@npm:0.1.21" +"@module-federation/runtime-tools@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/runtime-tools@npm:0.3.5" dependencies: - "@module-federation/runtime": 0.1.21 - "@module-federation/webpack-bundler-runtime": 0.1.21 - checksum: 628c0c4834093520f9c71481d587c9e18163f82e481b05b1900f04e2d5da4abb69af6d814ac5cd1951057b28d73f3adeb1cee7cd83628305b10cc7988405fbc5 + "@module-federation/runtime": 0.3.5 + "@module-federation/webpack-bundler-runtime": 0.3.5 + checksum: 5287783b12fff5a37d45cb904d9e004c569bb0803680c133a19002810f7c84f8bff87594dacbc473cdc813788eb8c9feb0915fb2f446ca0f4c609d0be9e33191 languageName: node linkType: hard -"@module-federation/runtime@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/runtime@npm:0.1.21" +"@module-federation/runtime@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/runtime@npm:0.3.5" dependencies: - "@module-federation/sdk": 0.1.21 - checksum: ce4de8515b54f1cd07a3c7c4cbd35fea163294b9fb24be10827872f3ebb62cd5c289f3602efe4149d963282739f79b51947afa039ee6f36be7f66dea83d590fc + "@module-federation/sdk": 0.3.5 + checksum: 61ef8a6535e80d583d5d755d33f4edad0b6e4df6118f38296dbab5eb5f23a624c8ca7ceab8c00d6d7d4249179615cff7235f3554476933aabbc9b988321944ae languageName: node linkType: hard -"@module-federation/sdk@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/sdk@npm:0.1.21" - checksum: 6856dcfe2ef5ae939890b82010aaad911fa6c4330a05f290ae054c316c9b532d3691456a1f9e176fe05f1df2d6f2d8c7e0c842ca5648a0fd7abf270e44ed9ecb +"@module-federation/sdk@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/sdk@npm:0.3.5" + checksum: 80f720af685def16bd80d6104dd7d831df03b1b3eedb11674a95f0e4420dbbaf732d4f619fc5e06421b36549483ab31484ad8a0490ac3033ed85ac300172be7d languageName: node linkType: hard -"@module-federation/third-party-dts-extractor@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/third-party-dts-extractor@npm:0.1.21" +"@module-federation/third-party-dts-extractor@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/third-party-dts-extractor@npm:0.3.5" dependencies: find-pkg: 2.0.0 fs-extra: 9.1.0 resolve: 1.22.8 - checksum: e394fd7c2e6dbdf8df6937628680e7356ac897ee6f1309d7fbc38c00bcf4be9c0363f8bc1a75c29f7987a5a2f11f7855481813889b18e8b444ee9006aeb4a299 + checksum: f7413bee4bf77fa8576dce31580fd91a636d0db6cffa17cd5f485039c5470254410005386f4b4160298b0fced985b9d1768411a64cc96f90a0b73f2e7d1a463f languageName: node linkType: hard -"@module-federation/webpack-bundler-runtime@npm:0.1.21": - version: 0.1.21 - resolution: "@module-federation/webpack-bundler-runtime@npm:0.1.21" +"@module-federation/webpack-bundler-runtime@npm:0.3.5": + version: 0.3.5 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.3.5" dependencies: - "@module-federation/runtime": 0.1.21 - "@module-federation/sdk": 0.1.21 - checksum: 7d96002066e63bdb503964fd5fb2798be25f4135a599d87721f4d26ebe1de1affbf447c56b082f7ee850ae7798d0ac637f6a486f58591269065e114051b466e5 + "@module-federation/runtime": 0.3.5 + "@module-federation/sdk": 0.3.5 + checksum: a4ba483be2ae1a157bcbaf1e895bbbe4c6cfe3039f6403ce543aa5163add4207c69d03f98a2b2910f21fbf61086f56c1f14ff19e44b36bb201a12ddf54ee3491 languageName: node linkType: hard @@ -14693,10 +14775,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.15.3": - version: 1.15.3 - resolution: "@remix-run/router@npm:1.15.3" - checksum: 9e70bd334d99fdf9285f0885c10353d7e25f66369080f551d997e3ce204e1af3a12d6f12b091f94a2dc9a54c80598bbe3c5194b57cbae17b7b40ab815dcd49a0 +"@remix-run/router@npm:1.18.0": + version: 1.18.0 + resolution: "@remix-run/router@npm:1.18.0" + checksum: dad8e8cbd3b44503c89de0fc5d8f92954d04c00bf36b42a18ae8b03fe0783742a99d0cae36f2962db38e2bdba3d40259105202c69620943e88f81711ceeb8fe4 languageName: node linkType: hard @@ -17965,11 +18047,11 @@ __metadata: linkType: hard "@types/http-proxy@npm:^1.17.10, @types/http-proxy@npm:^1.17.4, @types/http-proxy@npm:^1.17.8": - version: 1.17.14 - resolution: "@types/http-proxy@npm:1.17.14" + version: 1.17.15 + resolution: "@types/http-proxy@npm:1.17.15" dependencies: "@types/node": "*" - checksum: 491320bce3565bbb6c7d39d25b54bce626237cfb6b09e60ee7f77b56ae7c6cbad76f08d47fe01eaa706781124ee3dfad9bb737049254491efd98ed1f014c4e83 + checksum: d96eaf4e22232b587b46256b89c20525c453216684481015cf50fb385b0b319b883749ccb77dee9af57d107e8440cdacd56f4234f65176d317e9777077ff5bf3 languageName: node linkType: hard @@ -18564,6 +18646,13 @@ __metadata: languageName: node linkType: hard +"@types/pg-format@npm:^1.0.5": + version: 1.0.5 + resolution: "@types/pg-format@npm:1.0.5" + checksum: e7907b1e478b54aec581d14bc4df349d40d6e30e1f03605e9d162f515f35064e9deaeeb563cb812adfac271522c7ed68b13ebb6e2036d557c5533cf6357419bd + languageName: node + linkType: hard + "@types/pg-pool@npm:2.0.4": version: 2.0.4 resolution: "@types/pg-pool@npm:2.0.4" @@ -22344,6 +22433,15 @@ __metadata: languageName: node linkType: hard +"btoa@npm:^1.2.1": + version: 1.2.1 + resolution: "btoa@npm:1.2.1" + bin: + btoa: bin/btoa.js + checksum: afbf004fb1b1d530e053ffa66ef5bd3878b101c59d808ac947fcff96810b4452abba2b54be687adadea2ba9efc7af48b04228742789bf824ef93f103767e690c + languageName: node + linkType: hard + "buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -23202,17 +23300,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 @@ -24719,6 +24817,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" @@ -25657,8 +25762,8 @@ __metadata: linkType: hard "elliptic@npm:^6.0.0, elliptic@npm:^6.5.4": - version: 6.5.4 - resolution: "elliptic@npm:6.5.4" + version: 6.5.7 + resolution: "elliptic@npm:6.5.7" dependencies: bn.js: ^4.11.9 brorand: ^1.1.0 @@ -25667,7 +25772,7 @@ __metadata: inherits: ^2.0.4 minimalistic-assert: ^1.0.1 minimalistic-crypto-utils: ^1.0.1 - checksum: d56d21fd04e97869f7ffcc92e18903b9f67f2d4637a23c860492fbbff5a3155fd9ca0184ce0c865dd6eb2487d234ce9551335c021c376cd2d3b7cb749c7d10f4 + checksum: af0ffddffdbc2fea4eeec74388cd73e62ed5a0eac6711568fb28071566319785df529c968b0bf1250ba4bc628e074b2d64c54a633e034aa6f0c6b152ceb49ab8 languageName: node linkType: hard @@ -27004,6 +27109,8 @@ __metadata: resolution: "example-backend-legacy@workspace:packages/backend-legacy" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" @@ -27450,9 +27557,9 @@ __metadata: linkType: hard "fast-loops@npm:^1.1.3": - version: 1.1.3 - resolution: "fast-loops@npm:1.1.3" - checksum: b674378ba2ed8364ca1a00768636e88b22201c8d010fa62a8588a4cace04f90bac46714c13cf638be82b03438d2fe813600da32291fb47297a1bd7fa6cef0cee + version: 1.1.4 + resolution: "fast-loops@npm:1.1.4" + checksum: 8031a20f465ef35ac4ad98258470250636112d34f7e4efcb4ef21f3ced99df95a1ef1f0d6943df729a1e3e12a9df9319f3019df8cc1a0e0ed5a118bd72e505f9 languageName: node linkType: hard @@ -28887,17 +28994,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 @@ -32742,6 +32850,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" @@ -33524,6 +33641,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" @@ -33768,6 +33901,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" @@ -36887,6 +37027,13 @@ __metadata: languageName: node linkType: hard +"pg-format@npm:^1.0.4": + version: 1.0.4 + resolution: "pg-format@npm:1.0.4" + checksum: 159b43ad57d2f963f1072def86080dd2a6dd42c1a86046e388d47b491e00afe795139520eb01c8dffc43ac0243c77b3c4c5882d0ec5f488bb3281f17458b1b3d + languageName: node + linkType: hard + "pg-int8@npm:1.0.1": version: 1.0.1 resolution: "pg-int8@npm:1.0.1" @@ -38136,6 +38283,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" @@ -38921,15 +39075,15 @@ __metadata: linkType: hard "react-router-dom@npm:^6.3.0": - version: 6.22.3 - resolution: "react-router-dom@npm:6.22.3" + version: 6.25.1 + resolution: "react-router-dom@npm:6.25.1" dependencies: - "@remix-run/router": 1.15.3 - react-router: 6.22.3 + "@remix-run/router": 1.18.0 + react-router: 6.25.1 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 5ae3759a70e4123cd4b8efbb82199a69f5d8c4a7a434d186d2ec7b532b6ef3302df2a98e5c27db977d3f0d725c7a279010a16ae77a3bf6257f1fee96123d8b77 + checksum: fac768bc02cc0f39f5c1f022c8e7323ce9ffcc9b8e80456b51a70ff765854444cb4e6cdbf7d4132064a8fcbeeabeb82b8802614fbe66d8cbe7408e23ee33d789 languageName: node linkType: hard @@ -38944,14 +39098,14 @@ __metadata: languageName: node linkType: hard -"react-router@npm:6.22.3, react-router@npm:^6.3.0": - version: 6.22.3 - resolution: "react-router@npm:6.22.3" +"react-router@npm:6.25.1, react-router@npm:^6.3.0": + version: 6.25.1 + resolution: "react-router@npm:6.25.1" dependencies: - "@remix-run/router": 1.15.3 + "@remix-run/router": 1.18.0 peerDependencies: react: ">=16.8" - checksum: 1f7d9a5a849761ff69ef8f3d3131b4c1c25d18b76317ba5ad6f0d9421192c0b8b71ab0cc818c57aad7b81ada725559e513307d0ab43296a460262f0358602672 + checksum: 2abbab5ecc5d1210f70980571326fd1956da3898b78bfcce6229e346e658dd42ec75bea1caea6aa45607ef677e442f3241dc3fb839293f276fa4bd466870aadd languageName: node linkType: hard @@ -43422,6 +43576,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" @@ -44976,9 +45137,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.17.0": - version: 8.17.0 - resolution: "ws@npm:8.17.0" +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -44987,7 +45148,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 147ef9eab0251364e1d2c55338ad0efb15e6913923ccbfdf20f7a8a6cb8f88432bcd7f4d8f66977135bfad35575644f9983201c1a361019594a4e53977bf6d4e + checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf languageName: node linkType: hard